Skip to content

Commit 48bfd0b

Browse files
committed
DIP4242: Argument dependent attributes (ADAs)
This DIP aims to solve the common problem that arise when mixing delegates and attributes. It describes a language addition which the author feels will be natural to seasoned users that allow describing the relationship between a function's attribute and its callable(s) parameters.
1 parent 3624132 commit 48bfd0b

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed

DIPs/DIP4242.md

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# Argument-dependent attributes
2+
3+
| Field | Value |
4+
|-----------------|-----------------------------------------------------------------|
5+
| DIP: | (number/id -- assigned by DIP Manager) |
6+
| Review Count: | 0 (edited by DIP Manager) |
7+
| Author: | Mathias 'Geod24' Lang <at gmail dot com> |
8+
| Implementation: | Work in Progress |
9+
| Status: | Will be set by the DIP manager (e.g. "Approved" or "Rejected") |
10+
11+
## Abstract
12+
13+
Argument-dependent attributes are a means to express a function's attributes dependence
14+
on one or more delegate parameter.
15+
16+
They are a backward compatible change, extending the attributes syntax with an optional
17+
set of parenthesis containing an identifier list, in a fashion similar to that of UDAs.
18+
19+
A funtion fully-utilizing ADAs could look like this:
20+
```D
21+
void toString (scope void delegate(in char[]) sink) const
22+
@safe(sink) pure(sink) nothrow(sink) @nogc(sink)
23+
{
24+
sink("Hello World");
25+
}
26+
```
27+
28+
## Contents
29+
* [Rationale](#rationale)
30+
* [Prior Work](#prior-work)
31+
* [Description](#description)
32+
* [Breaking Changes and Deprecations](#breaking-changes-and-deprecations)
33+
* [Reference](#reference)
34+
* [Copyright & License](#copyright--license)
35+
* [Reviews](#reviews)
36+
37+
## Rationale
38+
39+
As of v2.095.0, there is no easy way to write a non-templated function that accepts
40+
a delegate parameter and allow a wide range of attributes.
41+
42+
For example, when writing a `@safe` function, one is faced with a restrictive choice:
43+
either marks the delegate as `@safe`, and force the caller to use `@trusted` in some occasions,
44+
or avoid marking the function itself `@safe`, and not be callable from `@safe` code.
45+
In general, the former seems to be the prefered approach, as it makes the most sense.
46+
47+
However, this choice is much less obvious for other attributes: forcing `nothrow`
48+
or `pure`ness is a less-accepted practice, let alone how restrictive forcing `@nogc` is.
49+
50+
This problem can be seen in many widely used library, such as Vibe.d's delegate-accepting
51+
[`requestHTTP`](https://vibed.org/api/vibe.http.client/requestHTTP),
52+
or even druntime's [`Throwable.toString`](https://github.com/dlang/druntime/blob/d97ec4093b108dc2fa95f1fa04b1114e6e0611f8/src/object.d#L2020-L2026).
53+
It is also commonly seen when implementing `opApply`, as one usually has to choose between working type-inferencex
54+
(e.g. `foreach (varname; container)` as opposed to `foreach (type varname; container)`),
55+
which only works if the delegate type is known and not templated, or supporting multiple
56+
attributes, which is done by templating the delegate type.
57+
58+
This proposal adds the ability to express the common dependency between a delegate's
59+
attributes and a function's attribute.
60+
61+
## Prior Work
62+
63+
[DIP1032](DIP1032.md) has proposed to always have the delegate parameters take the
64+
attributes of the function that receives it.
65+
However, this does not address the problem presented here: instead, it forces
66+
the user to require from the caller the attributes it supports.
67+
68+
Doing so usually leads to attributes being faked, or the routine being unusable:
69+
for example, iterating over a collection might be a simple operation which is
70+
`pure` and `@nogc`, but requiring the delegate to also be is too limitating.
71+
72+
## Description
73+
74+
### Grammar
75+
76+
The following changes to the grammar are proposed:
77+
```diff
78+
StorageClass:
79+
LinkageAttribute
80+
AlignAttribute
81+
deprecated
82+
enum
83+
static
84+
extern
85+
abstract
86+
final
87+
override
88+
synchronized
89+
auto
90+
scope
91+
const
92+
immutable
93+
inout
94+
shared
95+
__gshared
96+
Property
97+
nothrow
98+
+ nothrow AttributeDeclDef
99+
pure
100+
+ pure AttributeDeclDef
101+
ref
102+
103+
AtAttribute:
104+
@ disable
105+
@ nogc
106+
+ @ nogc AttributeDeclDef
107+
@ live
108+
Property
109+
@ safe
110+
+ @ safe AttributeDeclDef
111+
@ system
112+
@ trusted
113+
UserDefinedAttribute
114+
115+
+AttributeDeclDef:
116+
+ ( * )
117+
+ ( AttributeDeclDefs $(OPT) )
118+
119+
+AttributeDeclDefs:
120+
+ AssignExpression
121+
+ AssignExpression, AttributeDeclDefs ...
122+
```
123+
124+
This syntax was choosen as it should feel familiar to the user,
125+
who can already encounter it when using UDAs.
126+
127+
### Basic semantic rules
128+
129+
If the argument list form is used, the argument must be either identifiers
130+
or integers. If identifiers, they must match the identifiers of one of
131+
the function's arguments, and this argument must be a delegate or function pointer.
132+
If integers, the value must be positive and at most one less than the function's
133+
arity (number of parameter), included. The value must be the 0-based index of the
134+
argument the attribute depends on. Likewise, this argumment needs to be a
135+
delegate or function pointer.
136+
To avoid special cases in meta-programming code, we follow the widespread practice
137+
of allowing empty lists and trailing commas.
138+
139+
Hence, the following are valid:
140+
```D
141+
// Basic usage, with nothrow and @safe
142+
void func0(void delegate(int) sink) @safe(sink) nothrow(sink);
143+
// Empty argument list, equivalent to @safe
144+
void func1() @safe();
145+
// Equivalent to func0
146+
void func2(void delegate(int)) @safe(*) nothrow(*);
147+
// Equivalent to func0
148+
void func3(void delegate(int) arg) @safe(arg,) nothrow(*);
149+
// Equivalent to func1
150+
void func4(int) @safe(*);
151+
// Equivalent to func0
152+
void func3(void delegate(int) arg) @safe(0) nothrow(0,);
153+
```
154+
155+
However, the following are not valid:
156+
```D
157+
// Argument name does not exists
158+
void err0(void delegate() sink) @safe(snk);
159+
// Integer out of bound
160+
void err1(void delegate() sink) @safe(1);
161+
// Argument is not a delegate or function pointer
162+
void err2(int arg) @safe(arg);
163+
// Argument is not a delegate or function pointer
164+
void err3(int arg) @safe(0);
165+
```
166+
167+
### Call site checks vs callee checks
168+
169+
When checking the content of a function with ADA, the compiler must enforce the attributes
170+
applied to the function, *except* when calling the delegate argument.
171+
Hence, the following would error out:
172+
```D
173+
void semanticError(void delegate(in char[]) sink) nothrow(*)
174+
{
175+
if (sink is null)
176+
throw new Exception("Sink cannot be null"); // Error: `nothrow` function might `throw`
177+
sink("Hello World");
178+
}
179+
```
180+
181+
This is invalid because the function is not guaranteed to be `nothrow` even if `sink` is `nothrow`.
182+
However, as the attribute checks is pushed one level up (to the caller), the following code
183+
which currently errors out will now succeeds:
184+
```D
185+
void func(void delegate(in char[]) sink) nothrow(*)
186+
{
187+
sink("Hello World");
188+
}
189+
190+
void main() nothrow
191+
{
192+
func((in char[] arg) {
193+
printf("%.*s\n", cast(int) arg.length, arg.ptr);
194+
});
195+
}
196+
```
197+
198+
Finally, the check being performed in the caller means that invalid usage will still fail,
199+
such as in the following example:
200+
```D
201+
void func(void delegate(in char[]) sink) nothrow(*)
202+
{
203+
sink("Hello World");
204+
}
205+
206+
void main() nothrow
207+
{
208+
func((in char[] arg) {
209+
if (!arg.length)
210+
{
211+
// Error: `delegate` argument to `func` must be `nothrow` but may `throw`
212+
// `func` is called from `nothrow` context `main`
213+
throw new Exception("Length cannot be 0");
214+
}
215+
printf("%.*s\n", cast(int) arg.length, arg.ptr);
216+
});
217+
}
218+
```
219+
220+
### Interation with templates
221+
222+
The essence of ADAs targets non-templated code, as if the delegate type is templated,
223+
then the function's attributes can be infered definitively.
224+
However, there is nothing preventing the use of ADAs along with templates,
225+
for example, when the delegate type is not templated (or simply template-dependent):
226+
```D
227+
struct Container
228+
{
229+
int opApply (T) (scope void delegate(T)) @safe(*) nothrow(*);
230+
}
231+
```
232+
In this example, ADAs provide an improvement over the traditional approach:
233+
the delegate type is not templated, hence deduction is easier and error messages
234+
are more relevant, and only one function is generated per `T`.
235+
236+
This DIP does not recommend to make ADAs be inferred by the compiler.
237+
While the idea is attractive, the author estimate that the challenges posed
238+
by such a feature would at least double the amount of work required to get ADAs working.
239+
240+
## Breaking Changes and Deprecations
241+
242+
The change is purely additive, so no breaking changes are anticipated.
243+
244+
Additionally, an already annotated function can relax its requirements,
245+
switching from hard attributes to ADA in
246+
247+
## Reference
248+
249+
This idea, and the rationale, was presented with a focus on the problem space at DConf:
250+
- Presentation: https://dconf.org/2020/online/index.html#mathias
251+
- Live Q&A in parallel: **TODO**
252+
253+
## Copyright & License
254+
Copyright (c) 2020 by the D Language Foundation
255+
256+
Licensed under [Creative Commons Zero 1.0](https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt)
257+
258+
## Reviews
259+
The DIP Manager will supplement this section with a summary of each review stage
260+
of the DIP process beyond the Draft Review.

0 commit comments

Comments
 (0)