Comment #0 by stanislav.blinov — 2021-12-03T13:30:19Z
void consumeThingWithContext(Dg)(scope Dg dg)
if (is(Dg == delegate))
{
/* ... */
}
void consumeThingWithContext(S)(scope S s)
if (is(S == struct) && __traits(isNested, S))
{
/* ... */
}
@nogc
void testDelegate() // compiles
{
int a;
consumeThingWithContext({ a++; });
}
@nogc
void testStruct() // Error: function `testStruct` is `@nogc` yet allocates closures with the GC
{
int a;
struct Nested { ~this() @nogc { a++; } }
consumeThingWithContext(Nested());
}
---
IOTW, if there's no need to allocate context on the GC, it shouldn't be done.
Comment #1 by kinke — 2021-12-03T16:24:25Z
`scope S` doesn't prevent `consumeThingWithContext!Nested` from copying or moving the struct to some global state surviving the invocation.
Comment #2 by stanislav.blinov — 2021-12-03T17:20:08Z
It should in @safe code, which is where `scope` matters. The only way to escape it should be to break @safe. Isn't that the whole point?..
void* global;
struct Escapist(T)
{
T value;
}
@safe
void consumeThingWithContext(S)(scope S s)
if (is(S == struct) && __traits(isNested, S))
{
import core.lifetime;
global = s.tupleof[$-1]; // error: scope variable `s` assigned to non-scope `global`
global = move(s.tupleof[$-1]); // error: scope variable `s` assigned to non-scope `global`
auto escape = new Escapist!S(s); // with dip1000: scope variable `s` may not be copied into allocated memory
}
@safe /*@nogc*/
void main()
{
int a;
struct Nested { ~this() @nogc { a++; } }
consumeThingWithContext(Nested());
}
Comment #3 by stanislav.blinov — 2021-12-03T17:29:11Z
IOW, if there is a way to escape it, there's a bug.
Comment #4 by kinke — 2021-12-03T20:23:28Z
This currently compiles even with -dip1000:
```
void consumeThingWithContext(S)(scope S s)
if (is(S == struct) && __traits(isNested, S))
{
static S global;
global = s;
}
//@safe //@nogc
void main()
{
int a;
struct Nested { ~this() @safe @nogc { a++; } }
consumeThingWithContext(Nested());
}
```
It's just not inferred to be @safe.
It segfaults at runtime, presumably because `global` is initialized statically with null context pointer and cannot be successfully destructed prior to reassignment...
Comment #5 by stanislav.blinov — 2021-12-03T22:02:53Z
Well... the `static S s;` simply shouldn't compile at all for exactly that reason, so that's one extra bug:
struct WTF(T)
{
static T x;
void foo() { x.foo(); }
}
@safe
void main()
{
int a;
struct Nested
{
@safe:
void foo() { ++a; } // boom1
~this() { foo(); } // boom2
}
}
But the reason `consumeThingWithContext` is not inferred safe is the escape:
Error: scope variable `s` assigned to non-scope parameter `p` calling context.main.Nested.opAssign
The compiler is doing exactly the same thing for delegates too at the moment - it infers such escape as @system.
A delegate is context + one function pointer. A nested struct is effectively a superset of that, with an added advantage of type information for function pointers. There's no reason (other than implementation complexity) to support allocation elision for one and not the other.
Comment #6 by stanislav.blinov — 2021-12-03T22:04:47Z
Forgot to add the boom to main:
WTF!Nested.foo;
Comment #7 by robert.schadek — 2024-12-13T19:19:31Z