www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Pure Factory Functions =?UTF-8?B?8J+SlA==?= `inout`

reply Quirin Schroll <qs.il.paperinik gmail.com> writes:
In [Pure Factory 
Functions](https://dlang.org/spec/function.html#pure-factory-functions), it
says what conversions are enabled for the result of a strongly pure function
because the result is unique.

For one of my last PRs, I went through all possible combination 
of qualifiers and all pairs of these types. While I found no 
problematic conversion that was allowed, I did find conversions 
that should be allowed, but DMD rejects them. All of those 
involve `inout`; in particular, whatever of _mutable,_ `const`, 
and `immutable` you replace `inout` on both sides with (if at 
both sides), the conversion is allowed for a pure factory 
function, but not for `inout`. As far as I understand `inout`, 
this makes no sense and they should be allowed by D’s type system.

Can someone take a look at the list and tell me if I’m mistaken 
with some (or all) of them?

| “From” Type              | “To” Type                |
|--------------------------|--------------------------|
| _unshared_ `inout const` | `shared inout const`     |
| _unshared_ `inout`       | `shared inout`           |
| _unshared_ `inout`       | `shared inout const`     |
| _unshared_ `inout`       | _mutable unshared_       |
| _unshared_ `inout`       | _mutable_ `shared`       |
| _unshared_ `const`       | _unshared_ `inout const` |
| _unshared_ `const`       | `shared inout const`     |
| `shared inout const`     | _unshared_ `inout const` |
| `shared inout`           | _unshared_ `inout`       |
| `shared inout`           | _unshared_ `inout const` |
| `shared inout`           | _mutable unshared_       |
| `shared inout`           | _mutable_ `shared`       |
| `shared const`           | _unshared_ `inout const` |
| `shared const`           | `shared inout const`     |
| _mutable unshared_       | _unshared_ `inout`       |
| _mutable unshared_       | _unshared_ `inout const` |
| _mutable unshared_       | `shared inout`           |
| _mutable unshared_       | `shared inout const`     |
| _mutable_ `shared`       | _unshared_ `inout`       |
| _mutable_ `shared`       | _unshared_ `inout const` |
| _mutable_ `shared`       | `shared inout`           |
| _mutable_ `shared`       | `shared inout const`     |

[Here](https://wandbox.org/permlink/DHdKvSrfXO512Kxh) is the 
code, for those interested.
Feb 09 2023
parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On 2/9/23 12:41 PM, Quirin Schroll wrote:
 In [Pure Factory 
 Functions](https://dlang.org/spec/function.html#pure-factory-functions), 
 it says what conversions are enabled for the result of a strongly pure 
 function because the result is unique.
 
 For one of my last PRs, I went through all possible combination of 
 qualifiers and all pairs of these types. While I found no problematic 
 conversion that was allowed, I did find conversions that should be 
 allowed, but DMD rejects them. All of those involve `inout`; in 
 particular, whatever of _mutable,_ `const`, and `immutable` you replace 
 `inout` on both sides with (if at both sides), the conversion is allowed 
 for a pure factory function, but not for `inout`. As far as I understand 
 `inout`, this makes no sense and they should be allowed by D’s type system.
 
 Can someone take a look at the list and tell me if I’m mistaken with 
 some (or all) of them?
 
 | “From” Type              | “To”
Type                |
 |--------------------------|--------------------------|
 | _unshared_ `inout const` | `shared inout const`     |
 | _unshared_ `inout`       | `shared inout`           |
 | _unshared_ `inout`       | `shared inout const`     |
 | _unshared_ `inout`       | _mutable unshared_       |
 | _unshared_ `inout`       | _mutable_ `shared`       |
 | _unshared_ `const`       | _unshared_ `inout const` |
 | _unshared_ `const`       | `shared inout const`     |
 | `shared inout const`     | _unshared_ `inout const` |
 | `shared inout`           | _unshared_ `inout`       |
 | `shared inout`           | _unshared_ `inout const` |
 | `shared inout`           | _mutable unshared_       |
 | `shared inout`           | _mutable_ `shared`       |
 | `shared const`           | _unshared_ `inout const` |
 | `shared const`           | `shared inout const`     |
 | _mutable unshared_       | _unshared_ `inout`       |
 | _mutable unshared_       | _unshared_ `inout const` |
 | _mutable unshared_       | `shared inout`           |
 | _mutable unshared_       | `shared inout const`     |
 | _mutable_ `shared`       | _unshared_ `inout`       |
 | _mutable_ `shared`       | _unshared_ `inout const` |
 | _mutable_ `shared`       | `shared inout`           |
 | _mutable_ `shared`       | `shared inout const`     |
 
 [Here](https://wandbox.org/permlink/DHdKvSrfXO512Kxh) is the code, for 
 those interested.
a `pure` function which takes an `inout` reference and returns a `mutable` reference should always be implicitly convertible to anything you want. It's no different from `const` in this regard. Now, `inout` doesn't implicitly convert to/from `shared`. So throwing `shared` into the mix is likely to cancel some otherwise-correct conversions, but that's on the *argument* conversion, not the *return* conversion. The return conversion should always be valid. The idea is that the return value must be unique. I had a hard time following your code. Instead of using boilerplate generation, generate the boilerplate, run it, and then generate a complete file with the boilerplate that shows the conversions that don't work. -Steve
Feb 09 2023
parent reply Kagamin <spam here.lot> writes:
On Thursday, 9 February 2023 at 18:47:17 UTC, Steven 
Schveighoffer wrote:
 a `pure` function which takes an `inout` reference and returns 
 a `mutable` reference should always be implicitly convertible 
 to anything you want. It's no different from `const` in this 
 regard.
That's a weakly pure function, so its return value can convert only to const. Maybe it can convert to inout too, but the language doesn't recognize this pattern.
Feb 10 2023
parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On 2/10/23 7:53 AM, Kagamin wrote:
 On Thursday, 9 February 2023 at 18:47:17 UTC, Steven Schveighoffer wrote:
 a `pure` function which takes an `inout` reference and returns a 
 `mutable` reference should always be implicitly convertible to 
 anything you want. It's no different from `const` in this regard.
That's a weakly pure function, so its return value can convert only to const. Maybe it can convert to inout too, but the language doesn't recognize this pattern.
It can be weakly pure. The docs are wrong. i.e. this works: ```d pure int *foo(const int *p) { return new int(5); } void main() { int x; immutable int * bar = foo(&x); } ``` -Steve
Feb 10 2023
next sibling parent reply ag0aep6g <anonymous example.com> writes:
On Friday, 10 February 2023 at 14:00:20 UTC, Steven Schveighoffer 
wrote:
 It can be weakly pure. The docs are wrong.

 i.e. this works:

 ```d
 pure int *foo(const int *p)
 {
    return new int(5);
 }

 void main()
 {
    int x;
    immutable int * bar = foo(&x);
 }
 ```
That function "has no parameters with mutable indirections". So it's strongly pure, not weakly pure. I'd guess that `inout` counts like `const` with regards to weak/strong purity. So a `pure` function with `inout` parameters is also strongly pure.
Feb 10 2023
next sibling parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On 2/10/23 9:37 AM, ag0aep6g wrote:
 On Friday, 10 February 2023 at 14:00:20 UTC, Steven Schveighoffer wrote:
 It can be weakly pure. The docs are wrong.

 i.e. this works:

 ```d
 pure int *foo(const int *p)
 {
    return new int(5);
 }

 void main()
 {
    int x;
    immutable int * bar = foo(&x);
 }
 ```
That function "has no parameters with mutable indirections". So it's strongly pure, not weakly pure. I'd guess that `inout` counts like `const` with regards to weak/strong purity. So a `pure` function with `inout` parameters is also strongly pure.
I thought strong purity had to do with call elision/memoization. Clearly, calling with a const pointer can't be elided or memoized if passed a mutable. So the spec is consistent I guess, I just misunderstood what constituted "strong" purity. However, the Optimization section does state: An implementation may assume that a strongly pure function that returns a result without mutable indirections will have the same effect for all invocations with equivalent arguments. It is allowed to memoize the result of the function under the assumption that equivalent parameters always produce equivalent results." I feel like this is not a sound assumption if the parameter is const, and the argument is mutable. Of course, if the compiler can prove that the variable doesn't change elsewhere, then it's OK to memoize. But just assuming because it's strong pure is not correct. Of course, it depends on the definition of "equivalent arguments". -Steve
Feb 10 2023
parent reply ag0aep6g <anonymous example.com> writes:
On Friday, 10 February 2023 at 18:01:56 UTC, Steven Schveighoffer 
wrote:
 I thought strong purity had to do with call 
 elision/memoization. Clearly, calling with a const pointer 
 can't be elided or memoized if passed a mutable.

 So the spec is consistent I guess, I just misunderstood what 
 constituted "strong" purity.

 However, the Optimization section does state:

        An implementation may assume that a strongly pure 
 function that returns a result without mutable indirections 
 will have the same effect for all invocations with equivalent 
 arguments. It is allowed to memoize the result of the function 
 under the assumption that equivalent parameters always produce 
 equivalent results."

 I feel like this is not a sound assumption if the parameter is 
 const, and the argument is mutable. Of course, if the compiler 
 can prove that the variable doesn't change elsewhere, then it's 
 OK to memoize. But just assuming because it's strong pure is 
 not correct.

 Of course, it depends on the definition of "equivalent 
 arguments".
In my experience, David Nadlinger's "Purity in D"[1] is a better source of truth than DMD or the spec when it comes to `pure`. As far as I can tell, the spec and "Purity in D" use the same definitions for "weakly pure" and "strongly pure". David also describes "pure factory functions" (without calling them that) as "not [taking] any arguments with mutable indirections". So we got that right, too. But on memoization, David says: "When coming across a pure function with immutable parameters, only the identity of the arguments has to be checked in order to be able to optimize several calls down to one [...]. On the other hand, if an argument type contains indirections and is only const, somebody else could modify the data between two calls, requiring »deep« comparisons that might not be feasible for large data structures in the runtime case, or extensive data flow analysis in a compiler." Looks like the spec misses the requirement of having only immutable parameters. [1] https://klickverbot.at/blog/2012/05/purity-in-d/
Feb 10 2023
parent ag0aep6g <anonymous example.com> writes:
On Friday, 10 February 2023 at 19:33:26 UTC, ag0aep6g wrote:
 David also describes "pure factory functions" (without calling 
 them that) as "not [taking] any arguments with mutable 
 indirections".
Though I just realized that that can be interpreted two ways: 1. The function does not have any *parameters* with mutable indirections. I.e., `const` is ok. This is what DMD does. 2. The function cannot be called with mutable indirections. I.e., `const` is not ok. I think the first interpretation is the intended one, but I can't say I'm sure.
Feb 10 2023
prev sibling parent reply Quirin Schroll <qs.il.paperinik gmail.com> writes:
On Friday, 10 February 2023 at 14:37:35 UTC, ag0aep6g wrote:
 On Friday, 10 February 2023 at 14:00:20 UTC, Steven 
 Schveighoffer wrote:
 It can be weakly pure. The docs are wrong.

 i.e. this works:

 ```d
 pure int *foo(const int *p)
 {
    return new int(5);
 }

 void main()
 {
    int x;
    immutable int * bar = foo(&x);
 }
 ```
That function "has no parameters with mutable indirections". So it's strongly pure, not weakly pure.
Uniqueness analysis depends on the return type as much as on parameter types. In Steve’s example, `foo` does have parameters with mutable indirections (as I interpret that phrase – it’s honestly not precise wording) because `const` allows binding mutable values. The reason Steve’s example compiles is that by thorough investigation of parameter and return types, the compiler comes to the conclusion that there is no way `p`, which is a `const(int)*` makes it into the return value, and *therefore* the result must be uniquely created and thus may be cast to `immutable`. If you change the return type of `foo` to `const(int)*` (cf. `goo` in the example below), the parameter’s value could make it into the result and because `p` may be bound to mutable, the result cannot be cast to `immutable` implicitly: ```d pure safe int * foo(const(int)* p) { return new int(1); } pure safe const(int)* goo(const(int)* p) { return new int(1); } void main() safe { auto p = new int; immutable a = foo(p); // ok immutable b = goo(p); // error } ```
Feb 21 2023
parent reply ag0aep6g <anonymous example.com> writes:
On 21.02.23 11:46, Quirin Schroll wrote:
 On Friday, 10 February 2023 at 14:37:35 UTC, ag0aep6g wrote:
[...]
 That function "has no parameters with mutable indirections". So it's 
 strongly pure, not weakly pure.
Uniqueness analysis depends on the return type as much as on parameter types.
I was only commenting on weakly/strongly pure. As you say, the return type matters for pure factory functions. But it doesn't matter for weakly/strongly pure. All pure factory functions are strongly pure. Not all strongly pure functions are pure factory functions.
Feb 21 2023
next sibling parent reply Quirin Schroll <qs.il.paperinik gmail.com> writes:
On Tuesday, 21 February 2023 at 13:13:27 UTC, ag0aep6g wrote:
 On 21.02.23 11:46, Quirin Schroll wrote:
 On Friday, 10 February 2023 at 14:37:35 UTC, ag0aep6g wrote:
[...]
 That function "has no parameters with mutable indirections". 
 So it's strongly pure, not weakly pure.
Uniqueness analysis depends on the return type as much as on parameter types.
I was only commenting on weakly/strongly pure. As you say, the return type matters for pure factory functions. But it doesn't matter for weakly/strongly pure. All pure factory functions are strongly pure. Not all strongly pure functions are pure factory functions.
No, that is not true. The following `foo` is weakly pure, it mutates its input. But because the input cannot end up in the result (proven via their types, not the implementation), the result is unique, and can be converted to immutable. ```d safe: int* foo(double[] xs) pure { if (xs.length >= 2) xs[0] = xs[$ - 1]; return new int; } void main() { double[] xs = []; immutable a = foo(xs); } ``` The whole section of pure factory functions should be revised (it could moved to a textbook like Ali’s) because it wants to talk about unique results, but it talks about a special case of it.
Feb 21 2023
parent reply ag0aep6g <anonymous example.com> writes:
On 21.02.23 14:58, Quirin Schroll wrote:
 ```d
  safe:
 
 int* foo(double[] xs) pure
 {
      if (xs.length >= 2)
          xs[0] = xs[$ - 1];
      return new int;
 }
 
 void main()
 {
      double[] xs = [];
      immutable a = foo(xs);
 }
 ```
That right there is nothing but a safety hole. D allows casting from `double[]` to `int[]`, so the compiler cannot assume uniqueness in that case. More explosive test case: ---- safe: int[] foo(ubyte[] bytes) safe pure { return cast(int[]) bytes; /* the language very much allows this */ } void main() { ubyte[] bytes = [42, 0, 0, 0]; immutable ints = foo(bytes); assert(ints[0] == 42); /* passes */ bytes[0] = 13; assert(ints[0] == 42); /* fails; immutable int changed value */ } ----
Feb 21 2023
parent ag0aep6g <anonymous example.com> writes:
On 21.02.23 15:31, ag0aep6g wrote:
 On 21.02.23 14:58, Quirin Schroll wrote:
 ```d
  safe:

 int* foo(double[] xs) pure
 {
      if (xs.length >= 2)
          xs[0] = xs[$ - 1];
      return new int;
 }

 void main()
 {
      double[] xs = [];
      immutable a = foo(xs);
 }
 ```
That right there is nothing but a safety hole.
I did some digging and found `-preview=fixImmutableConv`. With that, the code fails compilation as it should. So this is a known issue and it's already in the process of getting fixed.
Feb 21 2023
prev sibling parent Quirin Schroll <qs.il.paperinik gmail.com> writes:
On Tuesday, 21 February 2023 at 13:13:27 UTC, ag0aep6g wrote:
 On 21.02.23 11:46, Quirin Schroll wrote:
 On Friday, 10 February 2023 at 14:37:35 UTC, ag0aep6g wrote:
[...]
 That function "has no parameters with mutable indirections". 
 So it's strongly pure, not weakly pure.
Uniqueness analysis depends on the return type as much as on parameter types.
I was only commenting on weakly/strongly pure. As you say, the return type matters for pure factory functions. But it doesn't matter for weakly/strongly pure. All pure factory functions are strongly pure. Not all strongly pure functions are pure factory functions.
No, that is not true. The following `foo` is weakly pure, it mutates its input. But because the input cannot end up in the result (proven via their types, not the implementation), the result is unique, and can be converted to immutable. ```d safe: int* foo(double[] xs) pure { if (xs.length >= 2) xs[0] = xs[$ - 1]; return new int; } void main() { double[] xs = []; immutable a = foo(xs); } ``` The whole section of pure factory functions should be revised (it could moved to a textbook like Ali’s) because it wants to talk about unique results, but it talks about a special case of it.
Feb 21 2023
prev sibling parent reply Kagamin <spam here.lot> writes:
On Friday, 10 February 2023 at 14:00:20 UTC, Steven Schveighoffer 
wrote:
 It can be weakly pure. The docs are wrong.
Looks like the check is deeper. This doesn't work: ```d struct A { const int[] a; } pure A a(const int[] b) { return A(b); } void e() { int[] b; immutable A c=a(b); } ```
Feb 10 2023
parent ag0aep6g <anonymous example.com> writes:
On Friday, 10 February 2023 at 15:23:04 UTC, Kagamin wrote:
 This doesn't work:

 ```d
 struct A
 {
 	const int[] a;
 }
 pure A a(const int[] b)
 {
 	return A(b);
 }
 void e()
 {
 	int[] b;
 	immutable A c=a(b);
 }
 ```
The spec currently says that the result of a pure factory function "has mutable indirections". It should probably say that the result "has *only* mutable indirections". Then it would be clear that `a` is not a pure factory function.
Feb 10 2023