On Wednesday, 28 February 2024 at 17:18:04 UTC, Paul Backus wrote:
> The primary goal of universal inference is to solve D's "attribute soup" problem without breaking compatibility with existing code. Compatibility with existing code makes universal inference a better solution to this problem than "@safe by default," "nothrow by default," and other similar proposals.
Overridable functions (that is, non-final virtual functions) are excluded from universal inference because their bodies may be replaced at runtime.
For cases where attribute inference is not desired, an opt-out mechanism will be provided.
Currently, .di
files generated by the compiler do not include inferred function attributes. This will have to change.
Related Links
I have a few improvements to the suggestions linked to above. (I'm packing all three of these into one post. But each is probably substantial enough to have its own subthread.)
Suggestion #1: Better syntax for the @default attribute DIP
More specifically, for the alternative mechanism proposed by the DIP, which aimed to provide a generic way of deactivating any currently active attribute, but was rejected in the DIP because the syntax was "too complex and verbose." Here is that syntax:
@nogc nothrow pure @safe:
// ...
void f(T)(T x) @nogc(default) @nothrow(default) pure(default) {}
I've seen some other suggestions for the same feature. But I didn't find any particularly compelling.
But just now I realized we could have: @no(...)
, where ...
contains the list of one or more attributes to deactivate.
So the above syntax transforms into:
@nogc nothrow pure @safe:
// ...
void f(T)(T x) @no(@nogc nothrow pure) {}
void g(T)(T x) @no(@nogc) @no(nothrow pure) {} // alternative grouping
@no(pure): // works for the scope too
...
This feature does not mean that the code it applies to wouldn't pass the checks for pure, @nogc, etc. It simply deactivates the previous label. The compiler can still infer a given attribute. It just wouldn't be explicit.
This syntax requires adding a keyword @no
, and the ability to group one or more attributes within parentheses. But that's all. It's very simple, and it makes generically deactivating attributes very easy.
Suggestion #2: Better syntax for the Argument Dependent Attributes DIP
This suggestion requires a little more elaboration to make clear. The DIP in question was linked to in Adam's article. First let's address the question of default attributes for a function which has callable parameter(s). What should the default inference behavior be for func()
in the following code? (Just focus on @safe
and throw
/nothrow
)
void func(void delegate(in char[]) sink) @safe {
sink("Hello World");
}
void g() {
func((in char[] arg) {
throw new Exception("Length cannot be 0");
});
}
void h() {
func((in char[] arg) {
return;
});
}
In my opinion, function func()
should be inferred @safe throw
when it is called in g()
, and @safe nothrow
when it is called in h()
. In other words, its attributes should be combined at the call site with those of the delegate which is passed to it. These are Argument Dependent Attributes (ADAs).
Moreover, this should be the default behavior. (Note: I'm not 100% certain about this, and would like to be shown otherwise. But I think it's true.)
In other words, any function which takes a delegate or a function as a parameter, should have Argument Dependent Attributes (ADAs) by default.
In the existing situation, however, we have no such attributes at all, let alone by default, and the DIP above suggests the following syntax in order to add them. (Hint: The *
means you don't have to specify the specific name of the argument for which the attribute status should propagate to the overall signature.):
// Basic usage, with nothrow and @safe
void func0(void delegate(int) sink) @safe(sink) nothrow(sink);
// Empty argument list, equivalent to @safe
void func1() @safe();
// Equivalent to func0
void func2(void delegate(int)) @safe(*) nothrow(*);
// Equivalent to func0
void func3(void delegate(int) arg) @safe(arg,) nothrow(*);
// Equivalent to func1
void func4(int) @safe(*);
// Equivalent to func0
void func3(void delegate(int) arg) @safe(0) nothrow(0,);
Again, the major problem here is with the chosen syntax. If I were suggesting a solution to the same problem, I would go with the following simple syntax using a new keyword @imply
:
void func0(void delegate(int) @imply sink);
@imply
simply means: Imply that (all) the attributes of sink()
apply to func0()
as well, and determine them each time func0()
is called. (@imply
as a keyword would only have any meaning as part of a callable parameter.) If you want to limit the implication to one or more particular attributes, indicate those in parentheses, using the same syntax as in Suggestion #1. So:
void func0(void delegate(int) @imply(@system throw) sink);
However, as mentioned above, I don't see why adding @imply
to every delegate/function passed as an argument shouldn't be the default behavior. After all, generally speaking, why have a callable as a function parameter if you're not going to call it in the body of the function?
Therefore, what we really need is to make @imply
the default, and add a way to opt out of it, by indicating that a function call should NOT infer its attributes based on the callable passed. So we are now defaulting to ADAs, and in rare cases adding @noimply()
to turn them off:
// @noimply turns the new default off for the specified attribute
void func(void delegate(in char[]) @noimply(throw) sink) @safe {
try {
sink("Hello World");
}
catch (Exception) {}
}
void g() {
func((in char[] arg) {
throw new Exception(".");
});
}
Since func()
above catches the exception, it can be determined and inferred to be nothrow
even if sink()
throws. @noimply(throw)
indicates this.
So, this is a syntax improvement suggestion for the ADAs DIP. But it's also a recognition that if they become the default, the primary need will be for an opt-out syntax rather than an opt-in one.
Suggestion #3: Syntax for explicitly annotating inferred attributes
> [From the OP:]"Currently, .di files generated by the compiler do not include inferred function attributes. This will have to change."
I assume that the problem is that the mangled names for a function should not include the inferred attributes even if the .di header files do. It may also help with documentation to be able to distinguish officially supported attributes from inferred ones.
For this, I suggest another keyword with the same syntax as my previous proposals. Namely, I suggest putting all inferred attributes into an @inferred()
grouping:
void func() @safe @nogc @inferred(pure nothrow);
@inferred()
has no effect other than to tell the compiler or the documentation generator that its contents, while accurate, are not part of the official API/ABI of the function. The compiler must naturally keep track of which attributes are explicit as opposed to inferred, in order to be able to generate headings like this.