What output do you think this has, and what do you think it should have / wish it had?

Code:

fn swap(a: anytype, b: anytype) void {
    if (@TypeOf(a) != @TypeOf(b))
        @compileError("cannot swap two variables of different type");
    a.*, b.* = .{ b.*, a.* };
}

test "swap" {
    var a: i32 = 1;
    var b: i32 = 2;
    swap(&a, &b);
    try std.testing.expectEqualSlices(i32, &.{ 2, 1 }, &.{ a, b });
}

Output:

-> zig build test
test
└─ run test 2/3 passed, 1 failed
error: 'main.test.swap' failed: slices differ. first difference occurs at index 1 (0x1)

============ expected this output: =============  len: 2 (0x2)

[0]: 2
[1]: 1

============= instead found this: ==============  len: 2 (0x2)

[0]: 2
[1]: 2

================================================

I am aware that the sheer size of this spoiler block gives it away ¯\_(ツ)_/¯

btw, this obviously works:

fn swap(a: anytype, b: anytype) void {
    if (@TypeOf(a) != @TypeOf(b))
        @compileError("cannot swap two variables of different type");
    const tmp = .{ b.*, a.* };
    a.*, b.* = tmp;
}

test "swap" {
    var a: i32 = 1;
    var b: i32 = 2;
    swap(&a, &b);
    try std.testing.expectEqualSlices(i32, &.{ 2, 1 }, &.{ a, b });
}

Another question: is this the most efficient assembly for a swap fn?

Its the most efficient I could find with zig code (and the one already in the stdlib)

swap:
        mov     eax, dword ptr [rdi]
        mov     ecx, dword ptr [rsi]
        mov     dword ptr [rdi], ecx
        mov     dword ptr [rsi], eax
        ret

Known issue.

2 Likes

Oops

I didnt know what do search for here. Thank you for the link!

It is. We’re doing one more copy than would be strictly necessary by a swap, but that is a limitation of x86, as it doesn’t have a mov memory to memory.

1 Like

I believe the behavior with RLS has changed slightly since that issue was first opened.

const std = @import("std");

const V2 = struct { x: f32, y: f32 };

test "typebrace" {
    var v = V2{ .x = 0, .y = 1 };
    v = V2{ .x = v.y, .y = v.x };
    try std.testing.expectEqual(V2{ .x = 1, .y = 0 }, v);
}

test "dotbrace" {
    var v = V2{ .x = 0, .y = 1 };
    v = .{ .x = v.y, .y = v.x };
    try std.testing.expectEqual(V2{ .x = 1, .y = 0 }, v);
}

The typebrace test passes, but dotbrace doesn’t. A typed initializer creates an intermediate value, while an unnamed initializer writes each field directly to the result location in order. This is documented further in the Result Locations section of the docs (it also provides a similar example to the test in the OP):

Expression Result Location Sub-expression Result Locations
.{x} ptr x has result location &ptr[0]
.{ .a = x } ptr x has result location &ptr.a
T{x} ptr x has no result location (typed initializers do not propagate result locations)
T{ .a = x } ptr x has no result location (typed initializers do not propagate result locations)

These subtleties are definitely prone to error, so I’m not necessarily defending the status quo and would personally like to see this addressed before 1.0. However, for now, a good rule of thumb is that if you need to swap two values in one line, you should make sure you’re using either the T{ b, a } or @as(T, .{ b, a }) forms and not a bare .{ b, a }.

3 Likes

This was in depth knowledge I did not expect to receive but I am not complaining :slight_smile:

const std = @import("std");

test "typed" {
    var a: u1 = 0;
    var b: u1 = 1;

    a, b = struct { u1, u1 }{ b, a };

    try std.testing.expectEqualSlices(u1, &.{ 1, 0 }, &.{ a, b });
}

test "infered" {
    var a: u1 = 0;
    var b: u1 = 1;

    a, b = .{ b, a };

    try std.testing.expectEqualSlices(u1, &.{ 1, 0 }, &.{ a, b });
}

Thats wild, on master as of today, typed passes but infered fails. Idk how I feel about that.
I honestly wish they would either just make it copy all of it and then maybe one day optimize as much on that behaveior as possible without the user noticing or say “its not happening all at once and we wont pretend that it is”, and document that nicely.

1 Like

this does create basically the same assembly as the stdlib version

Note that this is one of many examples of flaws with T{ ... } syntax (it can’t support RLS for subtle reasons relating to pointer coercions), which is why there’s an open proposal to remove it.

2 Likes

Thank you, I thought that const t = T{...}; and const t: T = .{...}; are exactly the same.
Could you please provide a bit more details or an example where RLS does not work?

Consider this:

const T = struct { f32 };
const S = struct { f64 };
fn foo() void {
    const x: S = T{ expr };
    _ = x;
}

…where expr is just some valid expression. This code is valid, because tuples coerce element-wise, and f32 can coerce to f64, so T can coerce to S. For RLS to work here, we start with a *S pointing to the stack location reserved for x; and we want to turn this into a *f32, probably going through a *T in the middle, so that expr can use that *f32 as its result location.

However, we can’t do that. A value of type f64 can’t be defined just by writing an f32 into memory, so likewise, we cannot define an S by writing a T into memory.

Funnily enough, since we’ve recently removed anonymous struct types from the language, there are no types with named fields that are distinct but coercible. That means, I think, that struct (rather than array) initializations (i.e. S{ .x = expr }) probably could forward result locations. However, having this be the case for struct initializations but not array initializations would be an inconsistent design.

The other issues with T{ ... } syntax (unrelated to RLS) are examined quite thoroughly by the issue I linked.

3 Likes

Is this actually RLS aliasing? I think it’s a more basic consequence of left-to-right execution order.

I read this:

As “copy the contents of address b to address a, then copy the contents of address a to address b”.

It certainly rhymes with some of the aliasing problems with RLS, but I think this is just a case of Zig’s sometimes-surprising execution order. In other words, this is an intended result which probably won’t change.

A less surprising way to illustrate this:

a.*, b.* = .{ 42, a.* };

These are both going to be 42, right?

This touches on an earlier conversation about how Zig’s execution semantics can be surprising, but are the right choice for the kind of language it is.

2 Likes

I see what you mean and honestly im not opposed to this behaveior, but it will have to be well documented.

2 Likes

For the sake of the curiosity, I’ve checked Hackers Delight’s which give also this solution:

x = xor(x, y)
y = xor(x, y)
x = xor(x, y)

But we can’t operate from memory to memory too…

1 Like

Have you ever seen XOR swap?

:rofl:

From a language specification consistency and understandability standpoint, I definitely think the execution ordering is much better than a special-casing rule to perform the swapping “all at once”.

Semi-related topic on importance of order in struct initialization:

1 Like

This won’t do what you want if x and y have the same value…

It is valid for any value. And it is easy to prove.

x = xor(x, y)
y = xor(x, y) = y'
x = xor(x, y) = x'

using single assignment:

a = xor(x, y)
b = xor(a, y) = y'
c = xor(a, b) = x'

y' = xor(a, y) = xor(xor(x, y), y) = xor(x, xor(y, y)) = xor(x, 0) = x
x' = xor(a, b) = xor(a, xor(a, y)) = xor(xor(a, a), y) = xor(0, y) = y
1 Like

Ah, right. The problem is aliasing, this works fine for values.

So same address, not same value.

It’s still an important caveat, because unless you’re working with addresses, ‘swapping’ is a meaningless concept.