Skip to content

Commit f29d650

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 f29d650

File tree

1 file changed

+290
-0
lines changed

1 file changed

+290
-0
lines changed

DIPs/DIP4242.md

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
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 `AttributeDeclDefs` form is used, the argument must be either identifiers or integers.
130+
131+
If identifiers, they must match the identifiers of one of the function's arguments,
132+
and this argument must be a delegate or function pointer.
133+
134+
If integers, the value must be positive and at most one less than the function's
135+
arity (number of parameter), included. The value must be the 0-based index of the
136+
argument the attribute depends on. Likewise, this argumment needs to be a
137+
delegate or function pointer.
138+
139+
To avoid special cases in meta-programming code, we follow the widespread practice
140+
of allowing empty lists and trailing commas.
141+
142+
For user's convenience, and to accomodate what is predicted to be the common case,
143+
the special token `*` can be used, which indicates that all delegate arguments of
144+
the functions are to be taken into account. Note that this is valid even if the
145+
function doesn't have any delegate argument.
146+
147+
Hence, the following are valid:
148+
```D
149+
// Basic usage, with nothrow and @safe
150+
void func0(void delegate(int) sink) @safe(sink) nothrow(sink);
151+
// Empty argument list, equivalent to @safe
152+
void func1() @safe();
153+
// Equivalent to func0
154+
void func2(void delegate(int)) @safe(*) nothrow(*);
155+
// Equivalent to func0
156+
void func3(void delegate(int) arg) @safe(arg,) nothrow(*);
157+
// Equivalent to func1
158+
void func4(int) @safe(*);
159+
// Equivalent to func0
160+
void func3(void delegate(int) arg) @safe(0) nothrow(0,);
161+
```
162+
163+
However, the following are not valid:
164+
```D
165+
// Argument name does not exists
166+
void err0(void delegate() sink) @safe(snk);
167+
// Integer out of bound
168+
void err1(void delegate() sink) @safe(1);
169+
// Argument is not a delegate or function pointer
170+
void err2(int arg) @safe(arg);
171+
// Argument is not a delegate or function pointer
172+
void err3(int arg) @safe(0);
173+
```
174+
175+
### Call site checks vs callee checks
176+
177+
When checking the content of a function with ADA, the compiler must enforce the attributes
178+
applied to the function, *except* when calling the delegate argument.
179+
Hence, the following would error out:
180+
```D
181+
void semanticError(void delegate(in char[]) sink) nothrow(*)
182+
{
183+
if (sink is null)
184+
throw new Exception("Sink cannot be null"); // Error: `nothrow` function might `throw`
185+
sink("Hello World");
186+
}
187+
```
188+
189+
This is invalid because the function is not guaranteed to be `nothrow` even if `sink` is `nothrow`.
190+
However, as the attribute checks is pushed one level up (to the caller), the following code
191+
which currently errors out will now succeeds:
192+
```D
193+
void func(void delegate(in char[]) sink) nothrow(*)
194+
{
195+
sink("Hello World");
196+
}
197+
198+
void main() nothrow
199+
{
200+
func((in char[] arg) {
201+
printf("%.*s\n", cast(int) arg.length, arg.ptr);
202+
});
203+
}
204+
```
205+
206+
Finally, the check being performed in the caller means that invalid usage will still fail,
207+
such as in the following example:
208+
```D
209+
void func(void delegate(in char[]) sink) nothrow(*)
210+
{
211+
sink("Hello World");
212+
}
213+
214+
void main() nothrow
215+
{
216+
func((in char[] arg) {
217+
if (!arg.length)
218+
{
219+
// Error: `delegate` argument to `func` must be `nothrow` but may `throw`
220+
// `func` is called from `nothrow` context `main`
221+
throw new Exception("Length cannot be 0");
222+
}
223+
printf("%.*s\n", cast(int) arg.length, arg.ptr);
224+
});
225+
}
226+
```
227+
228+
### Interation with templates
229+
230+
The essence of ADAs targets non-templated code, as if the delegate type is templated,
231+
then the function's attributes can be infered definitively.
232+
However, there is nothing preventing the use of ADAs along with templates,
233+
for example, when the delegate type is not templated (or simply template-dependent):
234+
```D
235+
struct Container
236+
{
237+
int opApply (T) (scope void delegate(T)) @safe(*) nothrow(*);
238+
}
239+
```
240+
In this example, ADAs provide an improvement over the traditional approach:
241+
the delegate type is not templated, hence deduction is easier and error messages
242+
are more relevant, and only one function is generated per `T`.
243+
244+
This DIP does not recommend to make ADAs be inferred by the compiler.
245+
While the idea is attractive, the author estimate that the challenges posed
246+
by such a feature would at least double the amount of work required to get ADAs working.
247+
248+
### Other callable types
249+
250+
The scope of this DIP is intentionally restricted to delegate and functions.
251+
However, the technique could be extended to other callables, such as `struct` or `class`
252+
that defines an `opCall`. Such an extension would address a similar issue
253+
encountered when writing OOP code, where using `interface` or base `class`
254+
and attributes does not compose well.
255+
256+
Such an extension could make the following code possible:
257+
```D
258+
interface Callable { void opCall (); }
259+
class Foo : Callable { override void opCall () nothrow {} }
260+
261+
// The next line currently doesn't compile because `Callable.opCall` is not `nothrow`
262+
void func (Callable c) @nothrow(*) { c(); }
263+
void main () nothrow
264+
{
265+
Foo f = new Foo();
266+
func(f); // This could work
267+
}
268+
```
269+
270+
## Breaking Changes and Deprecations
271+
272+
The change is purely additive, so no breaking changes are anticipated.
273+
274+
Additionally, an already annotated function can relax its requirements,
275+
switching from hard attributes to ADA in
276+
277+
## Reference
278+
279+
This idea, and the rationale, was presented with a focus on the problem space at DConf:
280+
- Presentation: https://dconf.org/2020/online/index.html#mathias
281+
- Live Q&A in parallel: **TODO**
282+
283+
## Copyright & License
284+
Copyright (c) 2020 by the D Language Foundation
285+
286+
Licensed under [Creative Commons Zero 1.0](https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt)
287+
288+
## Reviews
289+
The DIP Manager will supplement this section with a summary of each review stage
290+
of the DIP process beyond the Draft Review.

0 commit comments

Comments
 (0)