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