Attribute transference from callbacks?

Zach Tollen notmyrealemail at whatever.eee
Sun Dec 15 06:47:31 UTC 2024


On Wednesday, 11 December 2024 at 22:36:27 UTC, Manu wrote:
> On Wed, 11 Dec 2024 at 08:41, Zach Tollen via Digitalmars-d <
>> This was brought up in a [thread][thread1] in DIP Ideas. I 
>> contributed my idiosyncratic thoughts here:
>>
>> https://forum.dlang.org/post/rtvjsdyuqwmzwiggsolw@forum.dlang.org
>>
>> Suggestion #2 in that post addresses the issue you raise. In 
>> short, I proposed that the language default to inheriting 
>> attributes for the enclosing function at the call site, and 
>> then suggested a (callable function/delegate) parameter 
>> keyword `@noimply` for those rare cases when you *don't* want 
>> the function to inherit the characteristics of the delegate 
>> that you pass. I tried to explain it in the post.
>
> This appears to suffer from the same category of problem as 
> Schveighoffer's suggestion; it's that you can't reason about 
> the function statically anymore.

I'm pretty sure you can. I think there's no avoiding the fact 
that the the call site is only place where the proper behavior 
for these calls can be determined. The best the function 
definition can do is to let the caller know (via e.g., 
`@noimply`) whether the function is able to *negate* any 
`@system`/`throw`/@gc/impure effects (the latter two should be 
understood with their obvious meaning).

For example, a function could negate these effects by not 
actually *calling* the delegate. Or by other means, which vary 
depending on the attribute. But since negation is the rare case, 
by default the language should *assume*, I believe, that any 
delegate/function that is passed in as an argument *will* be 
called. Which is to say, all the inout(...) spam in your 
suggestion would work — but it shouldn't need to: it should work 
*implicitly*. It makes sense: If you pass in a delegate which is 
`@system`/`throw`/@gc/impure, the call should be treated as a 
`@system`/`throw`/@gc/impure action, unless specifically negated.

> Again, the reason I approach it from the perspective of 
> inout(...) is that you can always statically reason about it, 
> and its guarantees are appropriately applied at compile time.

My suggestion applies all the same guarantees at compile time. 
The negating effects of `@noimply` can be statically checked. For 
example:

```d
void f(void delegate() sink) {
     sink();
}

void g() nothrow {
     f(() { throw new Exception(""); } ); // Error: function f() 
called with delegate which has been inferred `throw`, in 
`nothrow` function g()
}
```
Let's tell it not to imply that the sink throws/doesn't throw:
```d
void h(void delegate() @noimply(throw) sink) {
     // The compiler tests the delegate by assuming sink() is 
@system/throw/@gc/impure
     sink(); // Error: delegate sink() is @noimply(throw), but may 
throw
}
```
The error above is the result of checking the delegate's 
attributes separately from the attributes for `h()`. This would 
happen for every use of a parameter delegate tagged with 
`@noimply`. (For parameters *not* tagged with `@noimply`, no 
checking is necessary — the call site propagates all the 
`@system`/`throw`/@gc/impure characteristics of the delegate that 
has been passed.)

In order to get it working, you have to make sure that the throw 
can't escape:
```d
void j(void delegate() @noimply(throw) sink) {
     // passes, statically checks that the call to sink() takes 
place in a context that can't throw
     try { sink(); }
     catch(Exception) {}
}

void k() nothrow {
     // passes(!) because the parameter in j() has been statically 
confirmed to not propagate the throw
     j(() { throw new Exception(""); } );
}
```
I believe this system has all the benefits of the other 
suggestions, but with almost no syntactic noise.


More information about the Digitalmars-d mailing list