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:
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:
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:
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.