Universal Function Attribute Inference
Zach Tollen
zach at notmyrealaddress.org
Wed May 1 18:35:32 UTC 2024
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
>
> * [Discussion of inference pros and cons][andrei-comment] by
> Andrei Alexandrescu
> * [Thoughts on inferred attributes][adr-post] by Adam Ruppe
> * [DIP70: @api/extern(noinfer) attribute][dip70]
> * [Add `@default` attribute][at-default]
>
> [andrei-comment]:
> https://github.com/dlang/dmd/pull/1877#issuecomment-16403663
> [adr-post]:
> http://dpldocs.info/this-week-in-d/Blog.Posted_2022_07_11.html#inferred-attributes
> [dip70]: https://wiki.dlang.org/DIP70
> [at-default]: https://github.com/dlang/DIPs/pull/236
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][at-default]**
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:
```D
@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:
```D
@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.
[at-default]: https://github.com/dlang/DIPs/pull/236
**Suggestion #2: Better syntax for the [Argument Dependent
Attributes DIP][ada-dip]**
This suggestion requires a little more elaboration to make clear.
The [DIP][ada-dip] in question was linked to in [Adam's
article][adr-post]. 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`)
```D
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][ada-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.):
```D
// 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`:
```D
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:
```D
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*:
```D
// @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][ada-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.
[ada-dip]:
https://github.com/dlang/DIPs/pull/198/files?short_path=d1fa190#diff-d1fa1908aafd30b6d5044235a23a348f294186638ec3af5dd4d71d455eab2302
[adr-post]:
http://dpldocs.info/this-week-in-d/Blog.Posted_2022_07_11.html#inferred-attributes
**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:
```D
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][api-vs-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.
[api-vs-abi]:
https://stackoverflow.com/questions/3784389/difference-between-api-and-abi
More information about the dip.ideas
mailing list