Bug 22563 – Nested structs, if not escaping, shouldn't allocate context (just like delegates)

Status
NEW
Severity
enhancement
Priority
P4
Component
dmd
Product
D
Version
D2
Platform
All
OS
All
Creation time
2021-12-03T13:30:19Z
Last change time
2024-12-13T19:19:31Z
Assigned to
No Owner
Creator
Stanislav Blinov
See also
https://issues.dlang.org/show_bug.cgi?id=15232
Moved to GitHub: dmd#18068 →

Comments

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
THIS ISSUE HAS BEEN MOVED TO GITHUB https://github.com/dlang/dmd/issues/18068 DO NOT COMMENT HERE ANYMORE, NOBODY WILL SEE IT, THIS ISSUE HAS BEEN MOVED TO GITHUB