May 18, 2021

On Tuesday, 18 May 2021 at 17:40:17 UTC, Max Haughton wrote:

>

On Tuesday, 18 May 2021 at 16:47:03 UTC, deadalnix wrote:

>

Long story short: https://issues.dlang.org/show_bug.cgi?id=21929

Closure do not respect scope the way they should. Let's fix it.

Time to consider by-value captures? Closures (specifically the way they interact with the GC) seem problematic in general, not just here.

No need, but a new closure is needed for each loop iteration in which there is a capture.

May 18, 2021
On 5/18/21 3:32 PM, Ola Fosheim Grostad wrote:
> On Tuesday, 18 May 2021 at 19:20:55 UTC, Steven Schveighoffer wrote:
>> On 5/18/21 3:04 PM, Ola Fosheim Grostad wrote:
>> There's also the issue that if you have a scoped variable that has a destructor, the value will be destroyed (and probably unusable) if you call the delegate from outside the scope.
> 
> Ouch. Ok, so in OO languages like Simula, all scopes are heap-closures and there is no stack, which kinda changes the game. I guess Javascript does the same conceptually but the JIT perhaps extracts uncaptured variables and puts those on a stack as an optimization? (My guess)
> 
> But how does a function with a delegate parameter know if it is safe to store the delegate or not?

Shouldn't matter. The compiler should not compile code that allows you to use a dangling struct (i.e. a destroyed struct).

In fact, it used to be this way, but there was a "hack" introduced to allow it to compile.

See: https://issues.dlang.org/show_bug.cgi?id=15952

And the change that allowed it (clearly identified as a hack): https://github.com/dlang/dmd/pull/5292/files#diff-a0928b0b76375204c6f58973fb3f2748e9e614394d5f4b0d8fa3cb20eb5a96c9R757-R760

I don't pretend to understand most of this, it was other sleuths (mostly Paul Backus) that discovered this.

-Steve
May 18, 2021
On Tuesday, 18 May 2021 at 20:01:15 UTC, Steven Schveighoffer wrote:
> I don't pretend to understand most of this, it was other sleuths (mostly Paul Backus) that discovered this.

I am not sure if my understanding of the language reference is correct, but I get a feeling this is an area where one just have to try different combinations and see what happens.


May 18, 2021
On 5/18/21 4:07 PM, Ola Fosheim Grostad wrote:
> On Tuesday, 18 May 2021 at 20:01:15 UTC, Steven Schveighoffer wrote:
>> I don't pretend to understand most of this, it was other sleuths (mostly Paul Backus) that discovered this.
> 
> I am not sure if my understanding of the language reference is correct, but I get a feeling this is an area where one just have to try different combinations and see what happens.
> 
> 

No, it was correct before the hack. Code which captured a struct that would be destroyed outside the scope just wouldn't compile. Now it does.

An example:

struct S
{
   bool destroyed = false;
   ~this() { destroyed = true; }
}

void main()
{
   void delegate() dg;
   {
     S s;
     dg = {writeln("destroyed = ", s.destroyed);};
     dg(); // destroyed = false
   }
   dg(); // destroyed = true
}

So basically, depending on when you call the delegate, the thing could be invalid. Not a big deal (maybe?) for a boolean, but could cause real problems for other things. And the user expectation is that when you capture the variable, it's how it was when you captured it. At least it should live as long as the delegate is alive, no?

-Steve
May 18, 2021
On Tuesday, 18 May 2021 at 20:26:26 UTC, Steven Schveighoffer wrote:
> So basically, depending on when you call the delegate, the thing could be invalid. Not a big deal (maybe?) for a boolean, but could cause real problems for other things. And the user expectation is that when you capture the variable, it's how it was when you captured it. At least it should live as long as the delegate is alive, no?

Yes, OO languages usually dont have destructors, so... Hm. I could see how this can go wrong, what if the captured object was "made" from an object in an outer scope that assumes that the inner scope objectrd is destructed before its lifetime is up?

I think it helps if we forget about stack and think of scopes as objects on a GC heap with links to the parent scope, they are kept alive as long as they are reachable, then destructed. But then we need to maintain the destruction order so that inner scopes are destructed first.

(what I meant in my previous post was that I need to experiment with delegate parameters and see hownit prevents stuff from escaping to fully grok it :-)



May 18, 2021
On Tuesday, 18 May 2021 at 20:26:26 UTC, Steven Schveighoffer wrote:
> On 5/18/21 4:07 PM, Ola Fosheim Grostad wrote:
> No, it was correct before the hack. Code which captured a struct that would be destroyed outside the scope just wouldn't compile. Now it does.

Btw in C++ lambdas should not outlive captured references, if you want that you need to make a copy, aka capture by value. That is a clean solution to the destructor problem as the destructor will be called when you expect it to.

I guess that is the only sensible solution.
May 18, 2021
On Tuesday, 18 May 2021 at 21:11:08 UTC, Ola Fosheim Grostad wrote:
> On Tuesday, 18 May 2021 at 20:26:26 UTC, Steven Schveighoffer wrote:
>> On 5/18/21 4:07 PM, Ola Fosheim Grostad wrote:
>> No, it was correct before the hack. Code which captured a struct that would be destroyed outside the scope just wouldn't compile. Now it does.
>
> Btw in C++ lambdas should not outlive captured references, if you want that you need to make a copy, aka capture by value. That is a clean solution to the destructor problem as the destructor will be called when you expect it to.
>
> I guess that is the only sensible solution.

Another correct solution is to track destruction and use a runtime liveness test before allowing object access. This would be more of a high level feature though. So you capture an object by reference, but you are not allowed to access a dead object.
May 19, 2021

On Tuesday, 18 May 2021 at 19:37:54 UTC, deadalnix wrote:

>

On Tuesday, 18 May 2021 at 17:40:17 UTC, Max Haughton wrote:

>

On Tuesday, 18 May 2021 at 16:47:03 UTC, deadalnix wrote:

>

Long story short: https://issues.dlang.org/show_bug.cgi?id=21929

Closure do not respect scope the way they should. Let's fix it.

Time to consider by-value captures? Closures (specifically the way they interact with the GC) seem problematic in general, not just here.

No need, but a new closure is needed for each loop iteration in which there is a capture.

https://d.godbolt.org/z/r4TKPa946 this pattern shouldn't go through the GC for example. If the delegate can fit a pointer it can fit an int, so going through the GC n times is a waste even before a proper solution is found.

May 18, 2021
On 5/18/2021 9:47 AM, deadalnix wrote:
> Long story short: https://issues.dlang.org/show_bug.cgi?id=21929
> 
> Closure do not respect scope the way they should. Let's fix it.

The simplest solution would be to disallow delegates referencing variables in scopes other than function scope.
May 18, 2021
On 5/18/2021 9:47 AM, deadalnix wrote:
> Long story short: https://issues.dlang.org/show_bug.cgi?id=21929
> 
> Closure do not respect scope the way they should. Let's fix it.

Let's rewrite it to something that does not use closures:

 int test() @safe {
    int j;
    int*[20] ps;

    for (int i = 0; i < 10; i++) {
        ps[j++] = &i;
    }

    for (int i = 0; i < 10; i++) {
        int index = i;
        ps[j++] = &index;
    }

    int x;
    foreach (p; ps)  {
        x += *p;
    }

    return x;
 }

This code is equivalent in terms of what is happening with references and scopes.

Compiling it with -dip1000 yields:

  Error: address of variable i assigned to ps with longer lifetime
  Error: address of variable index assigned to ps with longer lifetime

Which is pragmatically what the behavior of the delegate example would be, because the delegate is also storing a pointer to the variable.