Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DIP4242 (?): Argument dependent attributes (ADAs) #198

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
290 changes: 290 additions & 0 deletions DIPs/DIP4242.md
Original file line number Diff line number Diff line change
@@ -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 <at gmail dot com> |
| 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:
+ ( * )
Geod24 marked this conversation as resolved.
Show resolved Hide resolved
+ ( 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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The index version is quite confusing because of implicit boolean conversion (this looks like @safe(false) to me)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't think of that. The reason it's in is to support meta programming, where you really don't want to have to mess with identifiers. Sometimes you declare a function with a type tuple and in this case having ADAs support indexes is quite handy.

Copy link

@MoonlightSentinel MoonlightSentinel Dec 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't the same be achieved by allowing @safe(tuple[index]) to refer to the actual parameter inside of a parameter tuple?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how ? Or is tuple a keyword here ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the following function

void foo(T...)(T args) {}

Your current proposal suggests a plain index to specifiy a single parameter:

void foo(T...)(T args) @safe(0) {}

But this would be more expressive:

void foo(T...)(T args) @safe(args[0]) {}

Also a plain index doesn't work well with multiple variadic template parameters:

template bar(A...)
{
	void bar(B...)(A a, B b) @safe(1) {} // Might refer to an element of a or b
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, those are good points. Index based ADAs are a special case for when an argument has no identifier. Since the grammar allows it, I figured there should be a way to handle it.

But even if that's the intended usage, the semantics should be made clearer. And the tuple example is quite neat and more expressive, I agree. Let me think this over for a bit.

```

### 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.