January 20, 2023
On Friday, 20 January 2023 at 01:45:31 UTC, Walter Bright wrote:
>> void process(void delegate() userData) @nogc {}
>
> If process calls userData, then that is as it should be.

Ideally, you'd be able to say `process` inherits the nogc-ness of `userData`.

> userData is just being "passed through" and not called, then the easiest workaround is to cast it to something innocuous, or use a union.

But this is also a major pain. D's attributes are one of the biggest misses of the language.
January 19, 2023
On Thu, Jan 19, 2023 at 05:45:31PM -0800, Walter Bright via Digitalmars-d wrote:
> On 1/19/2023 5:14 AM, Adam D Ruppe wrote:
> > So if someone in a library wrote
> > 
> > void process(void delegate() userData) @nogc {}
> > 
> > it is going to force userData to be nogc regardless of their own desires.
> 
> If process calls userData, then that is as it should be. If userData is just being "passed through" and not called, then the easiest workaround is to cast it to something innocuous, or use a union.

This is a flaw in the language: there is no way to express that `process`'s attributes inherit from the passed-in delegate.  This has been a pain point for years.

The basic idea is that we want to ensure that the body of `process`, aside from the call(s) to userData, is pure/@nogc/@safe/etc., but we don't want to prevent the caller from passing an impure/GC/@system/etc. delegate (because it doesn't matter to the internal workings of `process`).  This makes `process` usable from both GC and @nogc code: if the caller is @nogc, then it must pass in a @nogc delegate.  But if the caller is GC allocating, then an allocating delegate is permitted, since the caller is already non-@nogc so a non-@nogc delegate will not change anything.

If we don't do it this way, there will always be a case that doesn't work.  If we make the delegate parameter @nogc, then non-@nogc code will not be able to pass in a delegate that GC-allocates.  If OTOH we try to be inclusive and make the delegate parameter unmarked, then @nogc code will not be able to call `process`.  The only workaround in this case is to invent `processWithGC` and `processWithNoGC` (because we cannot overload on @nogc/non-@nogc) with identical function bodies.  This is a waste, since the function bodies can be merged without changing any semantics at all.

The same argument applies to the other attributes: pure, @safe, nothrow, etc..


T

-- 
He who sacrifices functionality for ease of use, loses both and deserves neither. -- Slashdotter
January 19, 2023
On 1/19/2023 6:18 PM, Adam D Ruppe wrote:
> Ideally, you'd be able to say `process` inherits the nogc-ness of `userData`.

That's exactly what template attribute inference does.

January 19, 2023
On 1/19/2023 7:19 PM, H. S. Teoh wrote:
> This is a flaw in the language: there is no way to express that
> `process`'s attributes inherit from the passed-in delegate.  This has
> been a pain point for years.

void process()(void delegate() userData) {}

January 19, 2023
Consider:

 void delegate() dg;

 // no error
 void process(void delegate() userData) @nogc { dg = userData; }

 // Error: `@nogc` function `process2` cannot call non-@nogc delegate `userData`
 void process2(void delegate() userData) @nogc { userData(); }

If process() is made into a template, it will "inherit" @nogc based on whether the delegate is called or not.
January 19, 2023
On 1/19/2023 4:31 PM, H. S. Teoh wrote:
> Nobody wants to deal with attribute soup

<wry>
ImportC has to deal with the attribute soup of extensions added to various C compilers, all different. D is not so bad!
</wry>

January 19, 2023
On Thu, Jan 19, 2023 at 07:41:02PM -0800, Walter Bright via Digitalmars-d wrote:
> On 1/19/2023 7:19 PM, H. S. Teoh wrote:
> > This is a flaw in the language: there is no way to express that `process`'s attributes inherit from the passed-in delegate.  This has been a pain point for years.
> 
> void process()(void delegate() userData) {}

Code:

````
void process()(void delegate() cb) { cb(); }

void gcFunc() {
	int[] a;
	process({ a = new int[10]; });
}

void nogcFunc(void delegate() @nogc cb) @nogc {
	process(cb);
}
````

Compiler output:

````
/tmp/test.d(9): Error: `@nogc` function `test.nogcFunc` cannot call non-@nogc function `test.process!().process`
````

Nope, doesn't work.


T

-- 
Obviously, some things aren't very obvious.
January 20, 2023

On 1/19/23 10:38 PM, Walter Bright wrote:

>

On 1/19/2023 6:18 PM, Adam D Ruppe wrote:

>

Ideally, you'd be able to say process inherits the nogc-ness of userData.

That's exactly what template attribute inference does.

Not exactly what is being asked (though the quote implies it).

What is being asked is for process to inherit the nogc-ness of the argument passed to userData.

That is, you know process is calling userData, and you want that to figure into the inference (this doesn't technically require a template).

What we want is the effective attributes of the function to be the most restrictive possible of both the code inside the function, and the argument to the function.

The compiler would need a way to specify this for non-inferred functions, but it could be inferred for templates and auto functions.

-Steve

January 20, 2023

On Friday, 20 January 2023 at 00:31:54 UTC, H. S. Teoh wrote:

>

[…]

In general, I think we should move in the direction of expanding attribute inference as far as possible. Nobody wants to deal with attribute soup; it should be the compiler's job to automate this tedium as much as possible and only leave it up to the human when there's no other way around it.

I understand why Walter generally does not want inferred attributes for non-template functions. It increases build times and a non-template function simply is e.g. pure or not – it does not depend on anything outside the function or how it’s used. It would help if the compiler can be asked to inform you about sub-optimally annotated functions. It could be a compiler-flag (to check all functions compiled) and/or a pragma for checking on a per-module or per-function basis. It would list functions and the annotations they lack, but could have.

January 20, 2023

On Friday, 20 January 2023 at 03:41:02 UTC, Walter Bright wrote:

>

On 1/19/2023 7:19 PM, H. S. Teoh wrote:

>

This is a flaw in the language: there is no way to express that
process's attributes inherit from the passed-in delegate. This has
been a pain point for years.

void process()(void delegate() userData) {}

Er, no. The template process will be inferred @system thorw @gc impure because the delegate is annotated @system thorw @gc impure implicitly – unless you don’t actually call it. The correct version is this:

void process(DG : void delegate())(DG callback) { callback(); }

This has two drawbacks:

  1. process cannot be virtual.
  2. the argument bound to callback cannot have its parameter types inferred.

By 2. I mean the little more interesting example when the delegate takes parameters, say int:

/*A*/ void process(void delegate(int) callback) { callback(); }
/*B*/ void process(DG : void delegate(int))(DG callback) { callback(); }

(The constraint is optional, but useful.)

For version A, when process is called, like process((x){}), the compiler can statically infer that x is of type int. For version B, it cannot, but it does not matter practically for functions like process, however it does matter for opApply because programmers using a foreach loop really want the type of the iteration variable inferred (especially in meta programming); this inference only works when opApply is not a template. opApply may still be a template instance (or an alias to a template instance), which we can put to use. The shortest I could get to:

import std.meta : AliasSeq;
import std.traits : SetFunctionAttributes, functionLinkage, functionAttributes, FunctionAttribute;

template WithAnyCombinationOfAttributes(DG)
{
    alias WithAnyCombinationOfAttributes = AliasSeq!();
    static foreach (safety; [ FunctionAttribute.system, FunctionAttribute.safe ])
	static foreach (purity; [ FunctionAttribute.none, FunctionAttribute.pure_ ])
	static foreach (gcness; [ FunctionAttribute.none, FunctionAttribute.nogc ])
	static foreach (exness; [ FunctionAttribute.none, FunctionAttribute.nothrow_ ])
    {
        WithAnyCombinationOfAttributes = AliasSeq!(WithAnyCombinationOfAttributes, SetFunctionAttributes!(DG, functionLinkage!DG, (functionAttributes!DG & ~ FunctionAttribute.system) | safety | purity | gcness | exness));
    }
}

mixin template opApplyFromImpl(alias impl, protoDG, alias _WithAnyCombinationOfAttributes = WithAnyCombinationOfAttributes)
{
    static foreach (DG; _WithAnyCombinationOfAttributes!protoDG)
	alias opApply = impl!DG;
}

You can find the code in action here. In my opinion, something like this should be in Phobos.

The drawbacks are lots of template instantiations and that it always generates 16 overloads.