## Motivation
Result Location Semantics have three core goals in Zig
1. to aā¦llow returning large structs without a large perf cost
2. to allow async frames to be composed into structs without illegally copying them
3. to allow [pinned types](https://github.com/ziglang/zig/issues/7769) to be composed into structs without illegally copying them
Unfortunately this feature also causes some unintended side effects, like #4021, #3234, and #12064.
The problem looks like this:
```zig
const V2 = struct { x: f32, y: f32 };
test {
var v = V2{ .x = 0, .y = 1 };
v = V2{ .x = v.y, .y = v.x };
}
```
With the current semantics, the result location `&v` of the second assignment is forwarded to the struct initialization expression, causing it to be equivalent to
```zig
const result = &v;
result.x = v.y;
result.y = v.x;
```
This means that `v.x` is overwritten in the first assignment, and then used in the second assignment.
This is made worse by the transparent value-to-const-reference transformation which is performed on some struct parameters, which can cause aliasing to be easily observed, as in #3696 and #5973.
```zig
const V2 = struct { x: f32, y: f32 };
fn swap(v: V2) V2 {
return V2{ .x = v.y, .y = v.x };
}
test {
var v = V2{ .x = 0, .y = 1 };
v = swap(v);
}
```
Here these two features conspire to preserve aliasing through the function call. The parameter is transformed into a const reference and the result location is forwarded to the function. Both of these are `&v`, so this has the same behavior as the first example.
This behavior also has consequences for the optimizer. In order for an RLS transformation to be a strict win, the implicit return pointer needs to be marked `noalias`. However, this mark would turn the problems listed above into Unchecked Illegal Behavior, allowing the optimizer to mark them as `unreachable`. But without the `noalias` mark, the optimizer may in some cases be constrained and unable to combine writes to the result location due to concerns about aliasing. Additionally, since RLS applies to all structs, it includes very small structs that could be more efficiently returned in registers.
There are specific cases where we could detect this aliasing and make it Checked Illegal Behavior, but we would not be able to detect all cases. Consider as an example,
```zig
fn swizzle(v: *const [4]f32) [4]f32 {
return .{ v[1], v[2], v[3], v[0] };
}
test {
var arr = [5]f32{ 0, 1, 2, 3, 4 }
arr[0..4] = swizzle(&arr[1..5]);
}
```
Aliasing occurs here, but the only way to find it is to keep track of every read and write, and perform a pairwise comparison on all of them. Once loops get involved, the memory requirement increases for every iteration of the loop, as does the work required for detection. So it's unreasonable to make this checked in the general case.
Since it has occurred naturally in nonobvious ways, it shouldn't be UIB. But as shown above, we can't really make it CIB either. Implementation defined behavior here would only create a needless and difficult to observe portability burden. The alternative is to make it well-defined, and allow result pointers to alias. But that defeats the first core goal of RLS, to be an optimization. It also causes unexpected behavior, as shown by the linked issues. So something needs to change.
## The Fix
The obvious fix is to introduce temporaries on all assignments, but this would give up all of the three core goals. However, there are many cases where we can detect that a value must not alias, and safely preserve the current behavior. For example:
```zig
// x and y are new and cannot be referenced by anything, so it is safe to forward them directly to <expr>.
// We can guarantee that these do not create a temporary.
const x = <expr>;
var y = <expr>;
// with these changes, result locations are known not to alias, so they can be directly forwarded as well.
// So we can also guarantee that this does not create a temporary.
return <expr>;
```
The only cases that need to change are of the form
```zig
<existing_var_or_expr> = <expr>;
```
For these expressions, the existing var may alias inside the expr. For the sake of avoiding accidental aliasing, we should change the interpretation of these statements to do the following:
1. evaluate `&<existing_var_or_expr>`
2. create a temporary of `@TypeOf(<existing_var_or_expr>)`, or an anonymous temporary if it has an inferred type
3. evaluate `<expr>` using the temporary as the result location
4. copy from the temporary to %1
This change may have a performance cost in some cases, however it will also allow us to safely put the `noalias` attribute on result location pointers. Functions would be generated in the same way they are now, with an RLS pointer, but calls to those functions may need to introduce a temporary based on the form of the call.
## Forwarding Assignment Syntax
Sometimes you need to overwrite a large struct, and avoiding a copy is important for performance. After this change, you have two options:
1. Pass the result location yourself as a pointer.
2. Rely on the optimizer to detect no aliasing and remove the temporary
I think this is probably good enough for most use cases, but if there are convincing arguments we could consider introducing a new syntax that avoids the temporary and asserts that no aliasing will occur. A "forwarding assignment" where the lhs is used as an input to the rhs. I'd like to avoid syntax bikeshedding in this issue, but here are some examples of how it could look:
```zig
x => foo();
x := foo();
x <= foo(); // only superficially ambiguous, the compiler could easily disambiguate this
x nocopy = foo();
x noalias = foo();
@noalias(x) = foo();
```
## Consequences for Async and Pinned
Aside from optimization, async and pinned types have correctness requirements based on avoiding copies. Since this change will introduce copies which could break existing async code, we should implement compile errors for copying async frames before or at the same time as making this change. Once that's in though, most of async will still work with these changes. For example,
```zig
// guarantees no copy as above, no compile error here.
var frame = async foo();
// also no copy once #2765 is implemented
return frame;
```
But there are still places where you need to assign to existing memory. For example:
```zig
const frame = try allocator.alloc(@Frame(foo));
frame.* = async foo();
```
We have a sort of workaround for these cases, in the form of `@asyncCall`, but it's a pretty dramatic change in syntax for a small change in semantics. We don't have any parallel workaround for pinned structs. So for the sake of async and pinned, we may want to more strongly consider adding a Forwarding Assignment Syntax.
## Consequences for Parameter Passing
The value-to-const-reference conversion on parameters dovetails with the old RLS semantics to cause a lot of problems, as described at the beginning. However, with RLS "fixed" to avoid aliasing, I suspect that leaves it in a much better place. I think we should fix RLS first, and then see what problems still arise from parameter conversion and consider it separately if it is still causing problems.
## Consequences for Result Location References
These new temporaries may be observable by code using #2765, if code is allowed to reference the result location directly. However #2765 is not implemented yet so it won't affect any existing code.