www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Please, can the langauge stop inferring scope and return for

reply Dukc <ajieskola gmail.com> writes:
First thing first, I'm talking about code with 
`-preview=dip1000`. Make sure that is on if you test these.

There are times when you want to treat data in a scoped struct as 
non-scoped. Example:
```D
struct MyType
{ // Struct designed so that this will never point to local data
   private  system int* _content;
   // This might.
   int* otherPtr;
    trusted this(return ref int i)
   { _content = new int(i);
     otherPtr = &i;
   }
   // Note, not marked RETURN scope. Should return an unscoped 
pointer.
    trusted scope content(){return _content;}
}

 safe void main()
{ int* outer;
   if(true)
   { int inner;
     // Inferred as scope, which is intended
     auto myVar = MyType(inner);
     // Should be allowed
     outer = myVar.content;
   }
}
```

This pattern is perfectly  safe and makes sense. One problem 
though - it does not compile. Auto-inference being on, the 
compiler goes on to infer that `MyType.content` is `return scope`.

In this case, it would be easy to avoid this by simply disabling 
auto-inference by giving the function an explicit type: ` trusted 
pure nothrow  nogc scope int* content()`. This only works in 
non-templated functions and types though. We sometimes need to 
achieve the same inside a template too. We can do that:
```D
 trusted scope content()
{ auto tmp = _content;
   return tmp;
}
```

This works. The compiler does not really track lifetimes of the 
variables inside ` trusted` or ` system` functions. But it still 
tries to be smart if directly returning a pointer from the struct.

Frankly, this violates the principle of least astonishment pretty 
badly. Can't we just have a simple rule: if the function is 
explicitly marked ` trusted` or ` system`, NO `scope` or `return` 
attributes are inferred whatsoever?
May 19 2022
next sibling parent Dukc <ajieskola gmail.com> writes:
On Thursday, 19 May 2022 at 07:13:05 UTC, Dukc wrote:
 [snip]
Bugzilla entry created: https://issues.dlang.org/show_bug.cgi?id=23124
May 19 2022
prev sibling next sibling parent reply Tejas <notrealemail gmail.com> writes:
On Thursday, 19 May 2022 at 07:13:05 UTC, Dukc wrote:
 First thing first, I'm talking about code with 
 `-preview=dip1000`. Make sure that is on if you test these.

 [...]
The code compiled for me with `-preview=dip1000` in run.dlang.io though... And when I added a destructor that `free`d `_content`, and I added a `writeln(*outer);` below the `if` block, the program also segfaulted
May 19 2022
parent reply Dukc <ajieskola gmail.com> writes:
On Thursday, 19 May 2022 at 07:31:56 UTC, Tejas wrote:
 On Thursday, 19 May 2022 at 07:13:05 UTC, Dukc wrote:
 First thing first, I'm talking about code with 
 `-preview=dip1000`. Make sure that is on if you test these.

 [...]
The code compiled for me with `-preview=dip1000` in run.dlang.io though... And when I added a destructor that `free`d `_content`, and I added a `writeln(*outer);` below the `if` block, the program also segfaulted
Hmm, it seems to run with the default compiler, but fail to compile with nightly.
May 19 2022
parent reply bauss <jj_1337 live.dk> writes:
On Thursday, 19 May 2022 at 07:39:03 UTC, Dukc wrote:
 On Thursday, 19 May 2022 at 07:31:56 UTC, Tejas wrote:
 On Thursday, 19 May 2022 at 07:13:05 UTC, Dukc wrote:
 First thing first, I'm talking about code with 
 `-preview=dip1000`. Make sure that is on if you test these.

 [...]
The code compiled for me with `-preview=dip1000` in run.dlang.io though... And when I added a destructor that `free`d `_content`, and I added a `writeln(*outer);` below the `if` block, the program also segfaulted
Hmm, it seems to run with the default compiler, but fail to compile with nightly.
Probably shouldn't rely on nightly anyway, considering it's not fully tested, it could contain buggy/partial pull requests.
May 19 2022
parent reply Dukc <ajieskola gmail.com> writes:
On Thursday, 19 May 2022 at 07:46:15 UTC, bauss wrote:
 Probably shouldn't rely on nightly anyway, considering it's not 
 fully tested, it could contain buggy/partial pull requests.
I'm not sure quite what version the Symmetry-patched DMD I tested with was based on. It said "2.100.0" but if it is the stable 2.100.0 or something with nightly additions on top of it, I frankly don't know. Anyway I hope this addition won't make it to the next stable.
May 19 2022
next sibling parent Dukc <ajieskola gmail.com> writes:
On Thursday, 19 May 2022 at 08:30:58 UTC, Dukc wrote:
 I'm not sure quite what version the Symmetry-patched DMD I 
 tested with was based on. It said "2.100.0" but if it is the 
 stable 2.100.0 or something with nightly additions on top of 
 it, I frankly don't know.

 Anyway I hope this addition won't make it to the next stable.
run.dlang.io still runs 2.099.something.
May 19 2022
prev sibling parent reply Dennis <dkorpel gmail.com> writes:
On Thursday, 19 May 2022 at 08:30:58 UTC, Dukc wrote:
 Anyway I hope this addition won't make it to the next stable.
The reason run.dlang.io / dmd 2.099 allows it is that it failed to propagate lifetimes of `return ref` parameters through constructors, so it doesn't consider `myVar` `scope`. It would give an error about assigning `i` to the `otherPtr` member, but you marked the constructor ` trusted`. See: https://issues.dlang.org/show_bug.cgi?id=22801
May 19 2022
parent Dukc <ajieskola gmail.com> writes:
On Thursday, 19 May 2022 at 08:52:46 UTC, Dennis wrote:
 The reason run.dlang.io / dmd 2.099 allows it is that it failed 
 to propagate lifetimes of `return ref` parameters through 
 constructors, so it doesn't consider `myVar` `scope`. It would 
 give an error about assigning `i` to the `otherPtr` member, but 
 you marked the constructor ` trusted`. See:

 https://issues.dlang.org/show_bug.cgi?id=22801
Thanks. So it was a bug hiding another bug (or just controversial design - you decide).
May 19 2022
prev sibling next sibling parent reply Dennis <dkorpel gmail.com> writes:
On Thursday, 19 May 2022 at 07:13:05 UTC, Dukc wrote:
 Frankly, this violates the principle of least astonishment 
 pretty badly. Can't we just have a simple rule: if the function 
 is explicitly marked ` trusted` or ` system`, NO `scope` or 
 `return` attributes are inferred whatsoever?
There used to be different rules for lifetime errors in all of these: - explicit ` system` functions - ` system` by default functions (yes, [they were special](https://issues.dlang.org/show_bug.cgi?id=19873)) - inferred functions - ` safe` functions - ` trusted` functions It was really complex and confusing, and I've worked on simplifying it such that all lifetime errors are safety violations like any other. The only exception is directly returning a dangling pointer to a stack variable, which is just an error even in system code ([issue 19873](https://issues.dlang.org/show_bug.cgi?id=19873)). I don't want to go back to more special cases, especially with the dip1000 by default transition which is complex enough as is. I also don't agree with the "principle of least astonishment" violation. Consider this slice example: ```D struct Slice(T) { size_t length; private system T* _ptr; // ` safe` read-only access to _ptr T* ptr() trusted { return length == 0 ? null : _ptr; } } ``` I expect the compiler to infer `return scope` for `ptr()` here.
May 19 2022
parent reply Dukc <ajieskola gmail.com> writes:
On Thursday, 19 May 2022 at 09:29:50 UTC, Dennis wrote:
 I also don't agree with the "principle of least astonishment" 
 violation. Consider this slice example:

 ```D
 struct Slice(T)
 {
     size_t length;
     private  system T* _ptr;

     // ` safe` read-only access to _ptr
     T* ptr()  trusted { return length == 0 ? null : _ptr; }
 }
 ```

 I expect the compiler to infer `return scope` for `ptr()` here.
Sorry, I don't think that can be helped. The compiler can infer simple cases like this, but then it has to draw a line in water where it stops the inference as the function body gets more complex. You're going to be surprised anyway at some point if you expect inference from a ` trusted` function. It would probably be reasonable to have the compiler error in cases like this. But if the function compiles, there should be nothing implementation dependent about `scope`, `return scope` or lack thereof.
May 19 2022
parent reply Dennis <dkorpel gmail.com> writes:
On Thursday, 19 May 2022 at 09:44:29 UTC, Dukc wrote:
 but then it has to draw a line in water
 where it stops the inference as the function body gets more 
 complex. You're going to be surprised anyway at some point if 
 you expect inference from a ` trusted` function.
You get the same inference as a non-` trusted` function, it stops at the exact same point (i.e. as soon as aliases are created).
May 19 2022
parent reply Dukc <ajieskola gmail.com> writes:
On Thursday, 19 May 2022 at 09:57:47 UTC, Dennis wrote:
 You get the same inference as a non-` trusted` function, it 
 stops at the exact same point (i.e. as soon as aliases are 
 created).
Yes. But the point is, AFAIK it is implementation-defined how far a compiler will take checks like this. For example, this code: ```D system int* wrong() { int local; return &local; } ``` ...fails with DMD. But this is an implementation-defined error. Nothing in the spec says a compiler must check for this, and nothing in the spec says that the checks cannot be taken further than what DMD currently does. A little unportable, but reasonable. You can have helper functions like the one you just posted for cases like this, that are easy to port from one compiler to another in centralised fashion. But if such implementation-dependent checks cause the same function to compile with different semantics, then it's a problem. A trusted function might infer `return scope` of a parameter on one compiler and work well, but suddenly stop doing so for another. Then you have cryptic errors in another part of the application. Or even worse, you might escape pointers in the future due to the checks working differently than when you last tested them.
May 19 2022
next sibling parent Dukc <ajieskola gmail.com> writes:
On Thursday, 19 May 2022 at 10:21:00 UTC, Dukc wrote:
 On Thursday, 19 May 2022 at 09:57:47 UTC, Dennis wrote:
 You get the same inference as a non-` trusted` function, it 
 stops at the exact same point (i.e. as soon as aliases are 
 created).
Yes. But the point is, AFAIK it is implementation-defined how far a compiler will take checks like this.
Plus, that aliasing rule isn't exactly common documented knowledge. If the language behaviour depends on that, it's a recipe for confusion.
May 19 2022
prev sibling parent reply Dennis <dkorpel gmail.com> writes:
On Thursday, 19 May 2022 at 10:21:00 UTC, Dukc wrote:
 But if such implementation-dependent checks cause the same 
 function to compile with different semantics, then it's a 
 problem.
You're raising a valid concern, but this is a problem whether `scope` inference is enabled in ` trusted` code or not. One implementation could infer an unannotated function ` system` because it thinks `scope` pointers might escape, and a smarter implementation could find it's actually ` safe`, resulting in different mangling, different `is(F == G)`, different `__traits(compiles)` etc.
May 19 2022
parent reply Dukc <ajieskola gmail.com> writes:
On Thursday, 19 May 2022 at 10:48:13 UTC, Dennis wrote:
 You're raising a valid concern, but this is a problem whether 
 `scope` inference is enabled in ` trusted` code or not. One 
 implementation could infer an unannotated function ` system` 
 because it thinks `scope` pointers might escape, and a smarter 
 implementation could find it's actually ` safe`
As I understand it, an unannotated function, when inference is on, is checked just like it had ` safe` except that it's made ` system` instead of compilation failure if the check fails. You're probably right that the DIP1000 checking rules aren't well defined in the spec. But DIP1000 had a draft of those rules, meaning that they are at least intended to be specified - in principle they are not implementation-defined. Does the same apply to the more minimal escape checks in ` trusted`/` system`? I have always understood that they are intentionally implementation-defined checks. I think they must either be specified, or the langauge semantics must stop relying on them. I'd prefer the latter, as it does not feel as special cased.
May 19 2022
parent reply Dennis <dkorpel gmail.com> writes:
On Thursday, 19 May 2022 at 11:29:17 UTC, Dukc wrote:
 As I understand it, an unannotated function, when inference is 
 on, is checked just like it had ` safe` except that it's made 
 ` system` instead of compilation failure if the check fails. 
 (...) the more minimal escape checks in ` trusted`/` system`
The escape checks are not more minimal in ` trusted`/` system`, the only difference the attributes make is the behavior once it spots an error: - when the function is being inferred, it's set to ` system` - when the function is ` trusted`, ` system`, or ` system` by default, it ignores the error - when the function is ` safe`, it prints the error
May 19 2022
parent Dukc <ajieskola gmail.com> writes:
On Thursday, 19 May 2022 at 12:26:38 UTC, Dennis wrote:
 On Thursday, 19 May 2022 at 11:29:17 UTC, Dukc wrote:
 the more minimal escape checks in ` trusted`/` system`
The escape checks are not more minimal in ` trusted`/` system`, the only difference the attributes make is the behavior once it spots an error: - when the function is ` trusted`, ` system`, or ` system` by default, it ignores the error
If there is a check that does nothing on failure, there is no check from user perspective. With the more minimal checks, I meant the ones that can still silently add `return` to ` trusted` functions parameter.
May 23 2022
prev sibling parent reply Dennis <dkorpel gmail.com> writes:
On Thursday, 19 May 2022 at 07:13:05 UTC, Dukc wrote:
 There are times when you want to treat data in a scoped struct 
 as non-scoped.
I suggest using a helper function to cast away scope: ```D auto ref assumeNonScope(T)(auto ref scope T arg) { return *(cast(T*) cast(size_t) &arg); } struct MyType { // (...) trusted auto content() scope {return assumeNonScope(_content);} } ```
May 19 2022
parent Dukc <ajieskola gmail.com> writes:
On Thursday, 19 May 2022 at 09:37:32 UTC, Dennis wrote:
 I suggest using a helper function to cast away scope:
 ```D
 auto ref assumeNonScope(T)(auto ref scope T arg)
 {
     return *(cast(T*) cast(size_t) &arg);
 }

 struct MyType
 {
     // (...)
      trusted auto content() scope {return 
 assumeNonScope(_content);}
 }
 ```
Not bad. Worth considering.
May 19 2022