Jump to page: 1 2
Thread overview
swap
May 22
Dom DiSc
May 23
Dom DiSc
May 23
Dom DiSc
May 23
Dom DiSc
May 23
Dom DiSc
4 days ago
Dennis
4 days ago
Dom DiSc
4 days ago
Dennis
5 days ago
Dom DiSc
3 days ago
Paul Backus
3 days ago
Dom DiSc
3 days ago
Dom DiSc
3 days ago
Dom DiSc
2 days ago
Dom DiSc
2 days ago
Dom DiSc
May 22

Someone said without ref-counting or borrow-check it is not possible to create a @safe swap function.

I think this is not true, so I propose that phobos should provide the following swap-function which is usable in @safe code:

/// Generic swap function without temporary variables
/// Types with indirections need to define their own specific swap functions.
void swap(T)(ref T x, ref T y) @trusted if(is(typeof(x ^= y)) || !hasIndirections!T || isPointer!T || isArray!T)
{
   static if(is(typeof(x ^= y))) { x ^= y; y ^= x; x ^= y; } // numeric types
   else // flat structs, simple pointers and arrays
   {
      ubyte[] ax = (cast(ubyte*)&x)[0 .. T.sizeof];
      ubyte[] ay = (cast(ubyte*)&y)[0 .. T.sizeof];
      ax[] ^= ay[]; ay[] ^= ax[]; ax[] ^= ay[];
   }
}

@system unittest
{
   // type with xor operator
   int a = 100;
   int b = -123_456;
   swap(a,b);
   assert(a == -123_456);
   assert(b == 100);

   // basic array
   int[] c = [5,4,3,2,1];
   int[] d = [6,6,6,6];
   swap(c,d);
   assert(c == [6,6,6,6]);
   assert(d == [5,4,3,2,1]);

   // basic pointer (maybe to different types)
   void* e = &a;
   void* f = &c;
   swap(e,f);
   assert(e == &c);
   assert(f == &a);

   // flat struct with gaps
   struct F
   {
      ubyte x;
      short y;
      int z;
   }
   assert(!hasIndirections!F);
   assert(F.sizeof == 8); // not 7
   F p = F(100, -3, int.min);
   F q = F(0, 0, -1);
   (cast(byte*)&q)[1] = 2; // fill the gap with some garbage
   swap(p,q);
   assert(p.x==0 && p.y==0 && p.z==-1);
   assert(q.x==100 && q.y==-3 && q.z==int.min);
   assert((cast(byte*)&p)[1] == 2); // the garbage was swapped together with the data
   assert(!(p is F(0, 0, -1))); // memcmp() checks also the garbage in the gaps to be equal
   // assert(p != F(0, 0, -1)); // with -preview=fieldwise the default comparison is fixed
   assert(q == F(100, -3, int.min));
}
May 22

On Thursday, 22 May 2025 at 09:44:39 UTC, Dom DiSc wrote:

>

Someone said without ref-counting or borrow-check it is not possible to create a @safe swap function.

I think this is not true, so I propose that phobos should provide the following swap-function which is usable in @safe code:

Here's the unit test that a @safe swap function needs to be able to pass:

int* global;

@safe unittest
{
    int n;
    int* local = &n;

    swap(local, local);
    swap(global, global);
    assert(!__traits(compiles, swap(local, global)));
    assert(!__traits(compiles, swap(global, local)));
}

Your swap function does not pass this test--the 3rd and 4th assertions both fail (when compiling with -preview=dip1000 to allow taking the address of a local variable).

May 23

On Thursday, 22 May 2025 at 19:38:43 UTC, Paul Backus wrote:

>

Here's the unit test that a @safe swap function needs to be able to pass:

int* global;

@safe unittest
{
    int n;
    int* local = &n;

    swap(local, local);
    swap(global, global);
    assert(!__traits(compiles, swap(local, global)));
    assert(!__traits(compiles, swap(global, local)));
}

Your swap function does not pass this test--the 3rd and 4th assertions both fail (when compiling with -preview=dip1000 to allow taking the address of a local variable).

This is easy, but then it will also refuse to swap two local pointers
(which is kind of correct, because they have different lifetime):

// this will only compile if both x and y are global variables
void isGlobal(T)(ref T x, ref T y) @safe { x = y; }

void swap(T)(ref T x, ref T y) @trusted if(is(typeof(x ^= y))
   || !hasIndirections!T || isPointer!T || isArray!T)
{
   static if(is(typeof(x ^= y))) { x ^= y; y ^= x; x ^= y; }
   else
   {
      assert(__traits(compiles, isGlobal(x, y)));
      ubyte[] ax = (cast(ubyte*)&x)[0 .. T.sizeof];
      ubyte[] ay = (cast(ubyte*)&y)[0 .. T.sizeof];
      ax[] ^= ay[]; ay[] ^= ax[]; ax[] ^= ay[];
   }
}
May 23

On Friday, 23 May 2025 at 06:40:10 UTC, Dom DiSc wrote:

>
// this will only compile if both x and y are global variables
void isGlobal(T)(ref T x, ref T y) @safe { x = y; }

It's ugly to use this trick.
But the real problem is, that two local variables necessarily have different lifetimes, because they are at least declared one after the other. Even if I write

int* x, y;

the compiler assumes x has longer lifetime than y.

We need a way to enforce that two variables are considered to have the same lifetime, together with a new trait to check for that. E.g.

bool haveSameLifetime(...) { }

then we can allow only variables with same lifetime to be swapped.
We don't even need new syntax for this. The compiler simply should consider variables declared in the same statement to start their life simultaneous.

Of course this is a mayor change, but this is why I propose a DIP for it.

May 23

On Friday, 23 May 2025 at 09:45:52 UTC, Dom DiSc wrote:

>

On Friday, 23 May 2025 at 06:40:10 UTC, Dom DiSc wrote:

>
// this will only compile if both x and y are global variables
void isGlobal(T)(ref T x, ref T y) @safe { x = y; }

It's ugly to use this trick.
But the real problem is, that two local variables necessarily have different lifetimes, because they are at least declared one after the other. Even if I write

int* x, y;

the compiler assumes x has longer lifetime than y.

@safe unittest
{
    int n;
    int*[2] locals = [&n , &n];
    swap(locals[0], locals[1]); // same lifetime
}
May 23

On Friday, 23 May 2025 at 14:04:28 UTC, Paul Backus wrote:

>
@safe unittest
{
    int n;
    int*[2] locals = [&n , &n];
    swap(locals[0], locals[1]); // same lifetime
}

Yeah, now - this lying in your pocket. Because in this case we can have

void swap(T)(T[2] x) @trusted if(is(typeof(x[0] ^= x[1])) || !hasIndirections!T || isPointer!T || isArray!T)
{
   static if(is(typeof(x[0] ^= x[1]))) { x[0] ^= x[1]; x[1] ^= x[0]; x[0] ^= x[1]; } // numeric types
   else // flat structs, simple pointers and arrays
   {
      ubyte[] ax = (cast(ubyte*)&x[0])[0 .. T.sizeof];
      ubyte[] ay = (cast(ubyte*)&x[1])[0 .. T.sizeof];
      ax[] ^= ay[]; ay[] ^= ax[]; ax[] ^= ay[];
   }
}

int global;

@safe unittest
{
   int local;
   int*[2] mix = [&global , &local]; // equal lifetime ?!?
   swap(mix); // works, and dip1000 doesn't complain
}

So here we can swap globals with locals. Why shouldn't we if we use two parameters?
I would still like to have a trait to check for equal lifetime. And some better method to create variables of (really) equal lifetime.

May 23

On Friday, 23 May 2025 at 22:12:22 UTC, Dom DiSc wrote:

>

So here we can swap globals with locals.

Because dip1000 is too stupid to detect this.
Maybe we simply need a construct "assumeEqualLifetime(x,y)" so that the scope-check is turned off for those two variables and you need to check by hand.

5 days ago

On Thursday, 22 May 2025 at 19:38:43 UTC, Paul Backus wrote:

>

Here's the unit test that a @safe swap function needs to be able to pass:

int* global;

@safe unittest
{
    int n;
    int* local = &n;

    swap(local, local);
    swap(global, global);
    assert(!__traits(compiles, swap(local, global)));
    assert(!__traits(compiles, swap(global, local)));
}

Your swap function does not pass this test--the 3rd and 4th assertions both fail (when compiling with -preview=dip1000 to allow taking the address of a local variable).

After thinking a lot about this, I would say the third and forth assertion make only sense if by "local" it is meant "allocated on the stack", because swapping anything on the heap is always ok, no matter what lifetime the objects have.
And for taking the address of local objects I would say: if this is done, the object must be allocated on the heap, else it is always possible to run into problems, not only with swap.
And wasn't the precondition to allow taking the address of local objects exactly, that the compiler then automatically allocate them on the heap?

4 days ago

On Friday, 23 May 2025 at 22:20:42 UTC, Dom DiSc wrote:

>

On Friday, 23 May 2025 at 22:12:22 UTC, Dom DiSc wrote:

>

So here we can swap globals with locals.

Because dip1000 is too stupid to detect this.

What's happening is that the parameter of your @trusted function incorrectly gets inferred scope, since the compiler doesn't recognize the pointer assignment after you re-interpret cast to a ubyte[] view of the pointer bytes. The example makes me think scope should not be inferred on parameters of @trusted functions.

>

Maybe we simply need a construct "assumeEqualLifetime(x,y)" so that the scope-check is turned off for those two variables and you need to check by hand.

That idea of a @safe swap function is that you don't have to check anything by hand to ensure memory safety.

By the way, your opening post confuses me a little. You start by trying to prove you can make a @safe swap function, and then present a @trusted function, so I assume you want a swap function with a safe interface. If that's the case, then it's the function signature that matters, not the implementation.

Using the xor swap trick is emphasized as if it's part of the solution, but that just makes the function needlessly @trusted instead of @safe. It is possible to make a @safe swap function that passes your unittest, and the obvious implementation will do:

void swap(T)(ref T x, ref T y) @safe
{
    auto tmp = x;
    x = y;
    y = tmp;
}

Or alternatively: import std.algorithm.mutation: swap

Now there is a limitation to @safe swap: x and y must be regular 'infinite lifetime' pointers, not scope pointers. Creating a swap of 2 scope pointers isn't possible currently, but that has to do with the expressiveness of return scope attributes. You can express a function where y gets assigned to x like so:

void swap(ref scope T x, ref return scope T y)

But you can't express that x also gets assigned to y, because there's simply no syntax / implementation code for that.

4 days ago

On Tuesday, 27 May 2025 at 15:49:57 UTC, Dennis wrote:

>

That idea of a @safe swap function is that you don't have to check anything by hand to ensure memory safety.

I thought, we need some new construct to make that possible.
But see my last post. Now I don't understand where's the problem.

>

By the way, your opening post confuses me a little. You start by trying to prove you can make a @safe swap function, and then present a @trusted function, so I assume you want a swap function with a safe interface. If that's the case, then it's the function signature that matters, not the implementation.

Using the xor swap trick is emphasized as if it's part of the solution, but that just makes the function needlessly @trusted instead of @safe.

This is because for some objects it may be very expensive or even not possible to create a temporary copy. This is avoided by the xor-construct.

>

It is possible to make a @safe swap function that passes your unittest [...]
Now there is a limitation to @safe swap: x and y must be regular 'infinite lifetime' pointers, not scope pointers.

Yes, that's easy even with my function. See the version with "isGlobal" check.
But why is this limitation necessary? What can possibly go wrong?
You swap two valid objects, and after the first lifetime ends, one of the objects is deleted/freed/marked as garbage/has no valid pointer to it anymore. But does it matter which of the two it is (let by side the case one of the objects is allocated on the stack)?
Why and how does it matter? I can't see it. The lifetime of the two pointers is simply swapped together with the variables.

>

Creating a swap of 2 scope pointers isn't possible currently, but that has to do with the expressiveness of return scope attributes.

Yes, but why does the compiler need to know that the two have been swapped? They are valid objects of the same type. If one is deleted, does it matter which one?
I thought the GC will collect only objects with no valid pointer to it. So if the pointer of one of the two goes out of scope, it won't be collected if there are other pointers to it. It does not matter in which variable this valid address is stored (the current GC looks even in non-pointer variables for valid addresses).

« First   ‹ Prev
1 2