Bug 22270 – [DIP1000] class does infer scope in methods when assigned to a scope variable

Status
RESOLVED
Resolution
INVALID
Severity
normal
Priority
P1
Component
dmd
Product
D
Version
D2
Platform
All
OS
All
Creation time
2021-09-03T11:26:22Z
Last change time
2021-09-04T00:03:28Z
Assigned to
No Owner
Creator
João Lourenço

Comments

Comment #0 by jlourenco5691 — 2021-09-03T11:26:22Z
The struct can infer scope, but the class cannot. There are ways to get around this, but the behavior should be consistent. --- struct Bar { @safe void dummy() {} } class Foo { @safe void dummy() {} } @safe void main() { scope bar = new Bar; bar.dummy; // ok scope foo = new Foo; foo.dummy(); // fails } --- To work around this issue, the class function must be annotated with scope, or be a template.
Comment #1 by dkorpel — 2021-09-03T11:58:06Z
Neither method gets `scope` inferred. If you remove the `new` in Bar (`scope bar = Bar;`) you'll get the same error for the struct. What's happening is that since `Bar` is a pointer, it gets dereferenced implicitly: ``` @safe void main() { scope Bar* bar = new Bar; (*bar).dummy; // dereference strips away the scope layer } ``` `dummy` can't escape the pointer `bar` since `this` is passed by `ref`. Escaping a pointer to a ref variable (`&this`) is fixed by https://github.com/dlang/dmd/pull/12812. The class case is not allowed since dummy can do this: ``` Foo globalFoo; class Foo { @safe void dummy() { globalFoo = this; } } ``` So they are not consistent because `struct` is a value type and `class` a reference type.
Comment #2 by jlourenco5691 — 2021-09-03T14:28:41Z
So I just tested this and it compiled fine with DIP1000 --- struct Bar { @safe void dummy() {} } @safe void main() { scope bar = Bar(); bar.dummy; // ok } ---
Comment #3 by dkorpel — 2021-09-03T14:48:16Z
(In reply to João Lourenço from comment #2) > So I just tested this and it compiled fine with DIP1000 I forgot to mention, you also need to give `Bar` at least one pointer member. ``` struct Bar { int* x; @safe void dummy() {} } @safe void main() { scope bar = Bar(); bar.dummy; // ok } ``` onlineapp.d(11): Error: scope variable `bar` assigned to non-scope parameter `this` calling onlineapp.Bar.dummy
Comment #4 by jlourenco5691 — 2021-09-03T18:31:52Z
So, how can I achieve something like this while instantiating a class or struct with scope in @safe code? --- struct Bar { int[] somePointerToRestrictThis; @safe Wrapper dummy() return scope { Wrapper wrapper = { bar: &this }; return wrapper; } } struct Wrapper { Bar* bar; } @safe void main() { scope bar = Bar(); cast(void) bar.dummy; } ---
Comment #5 by jlourenco5691 — 2021-09-03T18:52:29Z
Shouldn't scope with struct instances behave the same way as a normal instantiation though? The example above works if I don't instantiate with scope.
Comment #6 by dkorpel — 2021-09-03T21:00:36Z
(In reply to João Lourenço from comment #4) > So, how can I achieve something like this while instantiating a class or > struct with scope in @safe code? You can't do that with @safe and dip1000 currently, because scope is only one layer. Making bar scope means making the array `somePointerToRestrictThis` scope, so dummy creates a pointer to a scope array which can't be expressed. What you can do is encapsulate `bar` in Wrapper and only give `return scope` access, and mark dummy @trusted: ``` struct Bar { int[] somePointerToRestrictThis; @trusted Wrapper dummy() return scope { Wrapper wrapper = { bar: &this }; return wrapper; } } struct Wrapper { private Bar* bar; // Must be explicitly `return scope` for our @trusted assumption @safe int[] access() return scope { return bar.somePointerToRestrictThis; } } ``` But of course you're on your own ensuring there's no holes in your @trusted code. > Shouldn't scope with struct instances behave the same way as a normal > instantiation though? The example above works if I don't instantiate > with scope. Not entirely sure what you mean, but return scope means scope in, scope out. If the input is not scope (and thus is assumed to have infinite lifetime), then the output does not need to have lifetime restrictions either.
Comment #7 by jlourenco5691 — 2021-09-03T21:40:05Z
I was referring to: ``` scope bar1 = Bar(); auto bar2 = Bar(); ``` I thought `scope` only meant instantiating on the stack when used this way. But I guess it also marks all fields as scope. If I understood correctly, the instance `bar1` lifetime is the current scope and so are all its members, and the instance `bar2` lifetime is the current scope but its members are assumed to have infinite lifetime. And that's why if instantiate Bar normally without scope I can do this in @safe code with dip1000: ``` struct Bar { int[] arr; @safe Wrapper dummy() { Wrapper wrapper = { bar: &this }; return wrapper; } } struct Wrapper { Bar* bar; } @safe void main() { auto bar = Bar(); cast(void) bar.dummy; } ```
Comment #8 by dkorpel — 2021-09-03T22:45:54Z
(In reply to João Lourenço from comment #7) > I was referring to: > > ``` > scope bar1 = Bar(); > auto bar2 = Bar(); > ``` > > I thought `scope` only meant instantiating on the stack when used this way. > But I guess it also marks all fields as scope. Scope *only* applies to the fields, there's nothing else it can apply to because a struct is a value type. A `struct S {int* x;}` is just an `int* x`. Both variables bar1 and bar2 are each just 16 bytes on the stack: the memory needed for the single int[] variable that is Bar's member (assuming a 64-bit target). When you take the address of a local variable `&bar1` or `&bar2`, then the resulting pointer necessarily needs to be `scope` since it points to stack memory, but this is something the compiler does based on its knowledge of where variables are declared, this is *not* what `scope` does in `scope bar1 = Bar();`. That declares the `int[]` member scope, which is why you can't take the address of `bar1`, since then you need a scope pointer to a scope array. > If I understood correctly, > the instance `bar1` lifetime is the current scope and so are all its > members, and the instance `bar2` lifetime is the current scope but its > members are assumed to have infinite lifetime. I think your confusion comes from the fact you think `scope` applies to the variable declaration, so you can have a `scope int` and then if you take the address of that you get a scope pointer. That's not an unreasonable thing to think, it's just not how it is designed in D. A `scope int` does nothing, a `scope int*` applies to the indirection.
Comment #9 by jlourenco5691 — 2021-09-04T00:03:28Z
Ah, got it! Thank you :)