diff --git a/DIPs/DIP4242.md b/DIPs/DIP4242.md new file mode 100644 index 000000000..1057ebb0f --- /dev/null +++ b/DIPs/DIP4242.md @@ -0,0 +1,290 @@ +# Argument-dependent attributes + +| Field | Value | +|-----------------|-----------------------------------------------------------------| +| DIP: | (number/id -- assigned by DIP Manager) | +| Review Count: | 0 (edited by DIP Manager) | +| Author: | Mathias 'Geod24' Lang | +| Implementation: | Work in Progress | +| Status: | Will be set by the DIP manager (e.g. "Approved" or "Rejected") | + +## Abstract + +Argument-dependent attributes are a means to express a function's attributes dependence +on one or more delegate parameter. + +They are a backward compatible change, extending the attributes syntax with an optional +set of parenthesis containing an identifier list, in a fashion similar to that of UDAs. + +A funtion fully-utilizing ADAs could look like this: +```D +void toString (scope void delegate(in char[]) sink) const + @safe(sink) pure(sink) nothrow(sink) @nogc(sink) +{ + sink("Hello World"); +} +``` + +## Contents +* [Rationale](#rationale) +* [Prior Work](#prior-work) +* [Description](#description) +* [Breaking Changes and Deprecations](#breaking-changes-and-deprecations) +* [Reference](#reference) +* [Copyright & License](#copyright--license) +* [Reviews](#reviews) + +## Rationale + +As of v2.095.0, there is no easy way to write a non-templated function that accepts +a delegate parameter and allow a wide range of attributes. + +For example, when writing a `@safe` function, one is faced with a restrictive choice: +either marks the delegate as `@safe`, and force the caller to use `@trusted` in some occasions, +or avoid marking the function itself `@safe`, and not be callable from `@safe` code. +In general, the former seems to be the prefered approach, as it makes the most sense. + +However, this choice is much less obvious for other attributes: forcing `nothrow` +or `pure`ness is a less-accepted practice, let alone how restrictive forcing `@nogc` is. + +This problem can be seen in many widely used library, such as Vibe.d's delegate-accepting +[`requestHTTP`](https://vibed.org/api/vibe.http.client/requestHTTP), +or even druntime's [`Throwable.toString`](https://github.com/dlang/druntime/blob/d97ec4093b108dc2fa95f1fa04b1114e6e0611f8/src/object.d#L2020-L2026). +It is also commonly seen when implementing `opApply`, as one usually has to choose between working type-inferencex +(e.g. `foreach (varname; container)` as opposed to `foreach (type varname; container)`), +which only works if the delegate type is known and not templated, or supporting multiple +attributes, which is done by templating the delegate type. + +This proposal adds the ability to express the common dependency between a delegate's +attributes and a function's attribute. + +## Prior Work + +[DIP1032](DIP1032.md) has proposed to always have the delegate parameters take the +attributes of the function that receives it. +However, this does not address the problem presented here: instead, it forces +the user to require from the caller the attributes it supports. + +Doing so usually leads to attributes being faked, or the routine being unusable: +for example, iterating over a collection might be a simple operation which is +`pure` and `@nogc`, but requiring the delegate to also be is too limitating. + +## Description + +### Grammar + +The following changes to the grammar are proposed: +```diff + StorageClass: + LinkageAttribute + AlignAttribute + deprecated + enum + static + extern + abstract + final + override + synchronized + auto + scope + const + immutable + inout + shared + __gshared + Property + nothrow ++ nothrow AttributeDeclDef + pure ++ pure AttributeDeclDef + ref + + AtAttribute: + @ disable + @ nogc ++ @ nogc AttributeDeclDef + @ live + Property + @ safe ++ @ safe AttributeDeclDef + @ system + @ trusted + UserDefinedAttribute + ++AttributeDeclDef: ++ ( * ) ++ ( AttributeDeclDefs $(OPT) ) + ++AttributeDeclDefs: ++ AssignExpression ++ AssignExpression, AttributeDeclDefs ... +``` + +This syntax was choosen as it should feel familiar to the user, +who can already encounter it when using UDAs. + +### Basic semantic rules + +If the `AttributeDeclDefs` form is used, the argument must be either identifiers or integers. + +If identifiers, they must match the identifiers of one of the function's arguments, +and this argument must be a delegate or function pointer. + +If integers, the value must be positive and at most one less than the function's +arity (number of parameter), included. The value must be the 0-based index of the +argument the attribute depends on. Likewise, this argumment needs to be a +delegate or function pointer. + +To avoid special cases in meta-programming code, we follow the widespread practice +of allowing empty lists and trailing commas. + +For user's convenience, and to accomodate what is predicted to be the common case, +the special token `*` can be used, which indicates that all delegate arguments of +the functions are to be taken into account. Note that this is valid even if the +function doesn't have any delegate argument. + +Hence, the following are valid: +```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,); +``` + +However, the following are not valid: +```D +// Argument name does not exists +void err0(void delegate() sink) @safe(snk); +// Integer out of bound +void err1(void delegate() sink) @safe(1); +// Argument is not a delegate or function pointer +void err2(int arg) @safe(arg); +// Argument is not a delegate or function pointer +void err3(int arg) @safe(0); +``` + +### Call site checks vs callee checks + +When checking the content of a function with ADA, the compiler must enforce the attributes +applied to the function, *except* when calling the delegate argument. +Hence, the following would error out: +```D +void semanticError(void delegate(in char[]) sink) nothrow(*) +{ + if (sink is null) + throw new Exception("Sink cannot be null"); // Error: `nothrow` function might `throw` + sink("Hello World"); +} +``` + +This is invalid because the function is not guaranteed to be `nothrow` even if `sink` is `nothrow`. +However, as the attribute checks is pushed one level up (to the caller), the following code +which currently errors out will now succeeds: +```D +void func(void delegate(in char[]) sink) nothrow(*) +{ + sink("Hello World"); +} + +void main() nothrow +{ + func((in char[] arg) { + printf("%.*s\n", cast(int) arg.length, arg.ptr); + }); +} +``` + +Finally, the check being performed in the caller means that invalid usage will still fail, +such as in the following example: +```D +void func(void delegate(in char[]) sink) nothrow(*) +{ + sink("Hello World"); +} + +void main() nothrow +{ + func((in char[] arg) { + if (!arg.length) + { + // Error: `delegate` argument to `func` must be `nothrow` but may `throw` + // `func` is called from `nothrow` context `main` + throw new Exception("Length cannot be 0"); + } + printf("%.*s\n", cast(int) arg.length, arg.ptr); + }); +} +``` + +### Interation with templates + +The essence of ADAs targets non-templated code, as if the delegate type is templated, +then the function's attributes can be infered definitively. +However, there is nothing preventing the use of ADAs along with templates, +for example, when the delegate type is not templated (or simply template-dependent): +```D +struct Container +{ + int opApply (T) (scope void delegate(T)) @safe(*) nothrow(*); +} +``` +In this example, ADAs provide an improvement over the traditional approach: +the delegate type is not templated, hence deduction is easier and error messages +are more relevant, and only one function is generated per `T`. + +This DIP does not recommend to make ADAs be inferred by the compiler. +While the idea is attractive, the author estimate that the challenges posed +by such a feature would at least double the amount of work required to get ADAs working. + +### Other callable types + +The scope of this DIP is intentionally restricted to delegate and functions. +However, the technique could be extended to other callables, such as `struct` or `class` +that defines an `opCall`. Such an extension would address a similar issue +encountered when writing OOP code, where using `interface` or base `class` +and attributes does not compose well. + +Such an extension could make the following code possible: +```D +interface Callable { void opCall (); } +class Foo : Callable { override void opCall () nothrow {} } + +// The next line currently doesn't compile because `Callable.opCall` is not `nothrow` +void func (Callable c) @nothrow(*) { c(); } +void main () nothrow +{ + Foo f = new Foo(); + func(f); // This could work +} +``` + +## Breaking Changes and Deprecations + +The change is purely additive, so no breaking changes are anticipated. + +Additionally, an already annotated function can relax its requirements, +switching from hard attributes to ADA in + +## Reference + +This idea, and the rationale, was presented with a focus on the problem space at DConf: +- Presentation: https://dconf.org/2020/online/index.html#mathias +- Live Q&A in parallel: **TODO** + +## Copyright & License +Copyright (c) 2020 by the D Language Foundation + +Licensed under [Creative Commons Zero 1.0](https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt) + +## Reviews +The DIP Manager will supplement this section with a summary of each review stage +of the DIP process beyond the Draft Review.