www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - pointer escaping return scope bug?

reply Nick Treleaven <nick geany.org> writes:
Hi,
The following seems like a bug to me (reduced code, FILE* changed 
to int*):
```d
 safe:

struct LockedFile
{
     private int* fps;

     auto fp() return scope => fps;
}

void main()
{
     int* p;
     {
         auto lf = LockedFile(new int);
         p = lf.fp;
     }
     assert(p != null); // address escaped
}
```
There's no error with -dip1000.
I'll file this unless I overlooked something.
Nov 19 2022
next sibling parent reply ag0aep6g <anonymous example.com> writes:
On 19.11.22 15:07, Nick Treleaven wrote:
 Hi,
 The following seems like a bug to me (reduced code, FILE* changed to int*):
 ```d
  safe:
 
 struct LockedFile
 {
      private int* fps;
 
      auto fp() return scope => fps;
 }
 
 void main()
 {
      int* p;
      {
          auto lf = LockedFile(new int);
          p = lf.fp;
      }
      assert(p != null); // address escaped
 }
 ```
 There's no error with -dip1000.
 I'll file this unless I overlooked something.
Let me rewrite `fp` as a static method. It's easier (for me) to understand what's going on that way: ---- static int* fp(return scope ref LockedFile that) { return that.fps; } ---- That's essentially just a function that returns its pointer parameter. So the program boils down to this: ---- safe: int* fp(return scope int* p) { return p; } void main() { int* p; { auto lf = new int; p = fp(lf); } assert(p != null); // address escaped } ---- Which is fine, as far as I can tell. `lf` is not `scope`. And when you pass it through `fp`, the result is still not `scope`. So escaping it is allowed. You do get an error when you make `lf` `scope` (explicitly or implicitly). So everything seems to be in order.
Nov 19 2022
parent reply Nick Treleaven <nick geany.org> writes:
On Saturday, 19 November 2022 at 14:52:23 UTC, ag0aep6g wrote:
 That's essentially just a function that returns its pointer 
 parameter. So the program boils down to this:

 ----
  safe:
 int* fp(return scope int* p) { return p; }
 void main()
 {
     int* p;
     {
         auto lf = new int;
         p = fp(lf);
     }
     assert(p != null); // address escaped
 }
 ----

 Which is fine, as far as I can tell. `lf` is not `scope`. And 
 when you pass it through `fp`, the result is still not `scope`. 
 So escaping it is allowed.

 You do get an error when you make `lf` `scope` (explicitly or 
 implicitly). So everything seems to be in order.
OK, so how do I make `lf` implicitly scope?
Nov 19 2022
next sibling parent ag0aep6g <anonymous example.com> writes:
On Saturday, 19 November 2022 at 15:02:54 UTC, Nick Treleaven 
wrote:
 OK, so how do I make `lf` implicitly scope?
By explicit/implicit I just meant this: ---- scope explicit = new int; int x; auto implicit = &x; ---- That's probably not helping with whatever you want to accomplish.
Nov 19 2022
prev sibling parent reply Dukc <ajieskola gmail.com> writes:
On Saturday, 19 November 2022 at 15:02:54 UTC, Nick Treleaven 
wrote:
 On Saturday, 19 November 2022 at 14:52:23 UTC, ag0aep6g wrote:
 That's essentially just a function that returns its pointer 
 parameter. So the program boils down to this:

 ```D
  safe:
 int* fp(return scope int* p) { return p; }
 void main()
 {
     int* p;
     {
         auto lf = new int;
         p = fp(lf);
     }
     assert(p != null); // address escaped
 }
 ```

 Which is fine, as far as I can tell. `lf` is not `scope`. And 
 when you pass it through `fp`, the result is still not 
 `scope`. So escaping it is allowed.

 You do get an error when you make `lf` `scope` (explicitly or 
 implicitly). So everything seems to be in order.
OK, so how do I make `lf` implicitly scope?
Have the `int*` inside it to point to a local, or assign another `scope int*` to it.
Nov 19 2022
parent reply Nick Treleaven <nick geany.org> writes:
On Saturday, 19 November 2022 at 15:24:33 UTC, Dukc wrote:
 On Saturday, 19 November 2022 at 15:02:54 UTC, Nick Treleaven 
 wrote:
 OK, so how do I make `lf` implicitly scope?
Have the `int*` inside it to point to a local, or assign another `scope int*` to it.
Thanks, this works: ```d safe: struct S { int* p; int[0] v; // dummy storage auto f() return trusted => p ? p : v.ptr; } void main() { int* p; { S s = S(new int); p = s.f; // error } } ```
Dec 15 2022
parent Nick Treleaven <nick geany.org> writes:
On Thursday, 15 December 2022 at 20:02:38 UTC, Nick Treleaven 
wrote:
     auto f() return  trusted => p ? p : v.ptr;
Whoops, that can't be trusted unless I `assert(p)`.
Dec 15 2022
prev sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Saturday, 19 November 2022 at 14:07:59 UTC, Nick Treleaven 
wrote:
 Hi,
 The following seems like a bug to me (reduced code, FILE* 
 changed to int*):
 ```d
  safe:

 struct LockedFile
 {
     private int* fps;

     auto fp() return scope => fps;
 }

 void main()
 {
     int* p;
     {
         auto lf = LockedFile(new int);
         p = lf.fp;
     }
     assert(p != null); // address escaped
 }
 ```
 There's no error with -dip1000.
 I'll file this unless I overlooked something.
I think this is intended behavior, because you *do* get an error if you replace `new int` with a pointer to a stack variable; e.g., int local; auto lf = LockedFile(&local); The `return scope` qualifier on the method does *not* mean "the return value of this method is `scope`". It means "this method may return one of this object's pointers, but does not allow them to escape anywhere else." In other words, it lets the compiler determine that the return value of `lf.fp` has *the same* lifetime as `lf` itself. Since, in your example, `lf` has global lifetime, the compiler deduces that `lf.fp` also has global lifetime, and therefore there is nothing wrong with assigning it to `p`.
Nov 19 2022
parent reply ShadoLight <ettienne.gilbert gmail.com> writes:
On Saturday, 19 November 2022 at 15:00:16 UTC, Paul Backus wrote:
 On Saturday, 19 November 2022 at 14:07:59 UTC, Nick Treleaven 
 wrote:
 Hi,
 The following seems like a bug to me (reduced code, FILE* 
 changed to int*):
 ```d
  safe:

 struct LockedFile
 {
     private int* fps;

     auto fp() return scope => fps;
 }

 void main()
 {
     int* p;
     {
         auto lf = LockedFile(new int);
         p = lf.fp;
     }
     assert(p != null); // address escaped
 }
 ```
 There's no error with -dip1000.
 I'll file this unless I overlooked something.
I think this is intended behavior, because you *do* get an error if you replace `new int` with a pointer to a stack variable; e.g., int local; auto lf = LockedFile(&local); The `return scope` qualifier on the method does *not* mean "the return value of this method is `scope`". It means "this method may return one of this object's pointers, but does not allow them to escape anywhere else." In other words, it lets the compiler determine that the return value of `lf.fp` has *the same* lifetime as `lf` itself. Since, in your example, `lf` has global lifetime, the compiler deduces that `lf.fp` also has global lifetime, and therefore there is nothing wrong with assigning it to `p`.
I follow your rationale, but for the life of me I cannot see how `lf` _"has global lifetime"_. Looks to me like `lf` is a value instance of the `LockedFile` struct (so on the stack) in a local scope inside main. I fully agree that the above code is not problematic, but isn't that because `p` is declared outside this local scope, and the allocation that happens inside the local scope (in the `lf` constructor) is on the heap, so the allocation (now assigned to `p`) survives the end of the local scope (and the end of the life of `lf`) since it is `p` that has global lifetime? I don't grok how `lf` can survive the local scope. Or am I missing something?
Nov 25 2022
next sibling parent reply ShadoLight <ettienne.gilbert gmail.com> writes:
On Friday, 25 November 2022 at 14:07:28 UTC, ShadoLight wrote:
 On Saturday, 19 November 2022 at 14:07:59 UTC, Nick Treleaven
 ```d
  safe:

 struct LockedFile
 {
     private int* fps;

     auto fp() return scope => fps;
 }

 void main()
 {
     int* p;
     {
         auto lf = LockedFile(new int);
         p = lf.fp;
     }
     assert(p != null); // address escaped
 }
 ```
[snip]
 I don't grok how `lf` can survive the local scope. Or am I 
 missing something?
Perhaps because the local scope is not pushed as a separate (anonymous) function on the stack... if true then, yes, then `lf` will indeed have the same physical lifetime as main (and `p`)...? On the other hand, if you add a destructor to `LockedFile`, it will be invoked at the end of the local scope, not the end of main. I find it a bit confusing what the term "lifetime" should pertain to in the case of variables declared inside a local scope inside a function - destructor invocation or physical existence of the variable on the stack? But this has no bearing on the heap allocation and the lifetime of `p` in the example.
Nov 25 2022
parent Nick Treleaven <nick geany.org> writes:
On Friday, 25 November 2022 at 15:03:57 UTC, ShadoLight wrote:
 I don't grok how `lf` can survive the local scope. Or am I 
 missing something?
Perhaps because the local scope is not pushed as a separate (anonymous) function on the stack... if true then, yes, then `lf` will indeed have the same physical lifetime as main (and `p`)...? On the other hand, if you add a destructor to `LockedFile`, it will be invoked at the end of the local scope, not the end of main.
Yes, the actual code does have a destructor. It's analogous to this: ```d safe: struct S { private int* fps; auto fp() return scope => fps; this(int) { fps = new int; } disable this(); disable void opAssign(S); ~this() trusted // how do we make this safe? { import core.memory; GC.free(fps); } } void main() { int* p; { auto lf = S(5); p = lf.fp; } assert(p != null); // address escaped } ``` That compiles with -dip1000. D doesn't seem to have a way to prevent the memory outliving the struct. Just to note there is another problem when the struct is destroyed explicitly whilst the `fp` result is still live. But that could be solved by making `object.destroy` and `std.algorithm.move` be restricted to system for certain structs like this one (either opt-in or some other mechanism). The first problem doesn't seem to have a solution.
Nov 27 2022
prev sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Friday, 25 November 2022 at 14:07:28 UTC, ShadoLight wrote:
 On Saturday, 19 November 2022 at 15:00:16 UTC, Paul Backus 
 wrote:
 Since, in your example, `lf` has global lifetime, the compiler 
 deduces that `lf.fp` also has global lifetime, and therefore 
 there is nothing wrong with assigning it to `p`.
I follow your rationale, but for the life of me I cannot see how `lf` _"has global lifetime"_.
You're right, my terminology here is sloppy. I'm really talking about the memory pointed to by `lf`, not `lf` itself, so I should really say that `lf` *points to memory* with global lifetime (or perhaps "`*lf` has global lifetime").
Nov 25 2022
parent Tejas <notrealemail gmail.com> writes:
On Friday, 25 November 2022 at 17:45:57 UTC, Paul Backus wrote:
 On Friday, 25 November 2022 at 14:07:28 UTC, ShadoLight wrote:
 On Saturday, 19 November 2022 at 15:00:16 UTC, Paul Backus 
 wrote:
 Since, in your example, `lf` has global lifetime, the 
 compiler deduces that `lf.fp` also has global lifetime, and 
 therefore there is nothing wrong with assigning it to `p`.
I follow your rationale, but for the life of me I cannot see how `lf` _"has global lifetime"_.
You're right, my terminology here is sloppy. I'm really talking about the memory pointed to by `lf`, not `lf` itself, so I should really say that `lf` *points to memory* with global lifetime (or perhaps "`*lf` has global lifetime").
Time to use separation logic
Nov 25 2022