Adjusted nosuchmethod-forwarding.md

This CL changes said document to drop support for requested generation
of forwarder in case of a conflict. Developers must then write a
disambiguating method implementation themselves.

Note that this is what we already agreed during discussions about
this feature, but I noted that it hadn't yet been written into the
document.

Also changes the status to 'under implementation'.

Change-Id: I31b3dd8d65438484824225ad7067d36462d26aa7
Reviewed-on: https://dart-review.googlesource.com/22421
Commit-Queue: Erik Ernst <eernst@google.com>
Reviewed-by: Leaf Petersen <leafp@google.com>
This commit is contained in:
Erik Ernst 2017-12-01 12:39:19 +00:00
parent 8e4b0bda23
commit a6d422957d

View file

@ -1,16 +1,16 @@
## Feature: No Such Method Forwarding
## NoSuchMethod Forwarding
Author: eernst@
**Status**: Under discussion.
**Status**: Under implementation.
**Version**: 0.3 (2017-10-05)
**Version**: 0.5 (2017-11-27)
**This document** is an informal specification of the support in Dart 2 for
invoking `noSuchMethod` in situations where an attempt is made to invoke a
method that does not exist.
**The feature** described here, *no such method forwarding*, is a particular
**The feature** described here, *noSuchMethod forwarding*, is a particular
approach whereby an implementation of `noSuchMethod` in a class _C_ causes
_C_ to be extended with a set of compiler generated forwarding methods, such
that an invocation of any method in the static interface of _C_ will become
@ -25,13 +25,12 @@ call a method that does not exist.
In other words, consider an instance method invocation of a member named
_m_ on a receiver _o_ whose class _C_ does not have a member named _m_ (or
it has a member named _m_, but it does not admit the given invocation,
e.g., because the number of arguments is wrong). If _C_ declares or inherits
an implementation of the method `noSuchMethod` which is distinct from the
one in the built-in class `Object`, the properties of the invocation are
specified using an instance _i_ of `Invocation`, and `noSuchMethod` is then
invoked with _i_ as the actual argument. Among other things, _i_ specifies
whether the invocation was a method call or an invocation of a getter or a
setter, and it specifies which actual arguments were passed.
e.g., because the number of arguments is wrong). The properties of the
invocation are then specified using an instance _i_ of `Invocation`, and
`noSuchMethod` is then invoked with _i_ as the actual argument. Among other
things, _i_ specifies whether the invocation was a method call or an
invocation of a getter or a setter, and it specifies which actual arguments
were passed.
One difficulty with this design is that it requires developers to take
both method invocations and getter invocations into account, in order to
@ -74,8 +73,8 @@ a large number of methods with complex signatures. It is particularly
inconvenient if the mock behavior is simple and largely independent of
all those types.
The no such method forwarding approach eliminates much of this tedium
by means of compiler-generated forwarding methods corresponding to all
The noSuchMethod forwarding approach eliminates much of this tedium
by means of compiler generated forwarding methods corresponding to all
the unimplemented methods. The example could then be expressed as
follows:
```dart
@ -96,7 +95,7 @@ class MockFoo implements Foo {
}
}
```
With no such method forwarding, this causes a `foo` forwarding
With noSuchMethod forwarding, this causes a `foo` forwarding
method to be generated, with the signature declared in `Foo`
and with the necessary code to create and initialize a suitable
`Invocation` which will be passed to `noSuchMethod`.
@ -104,7 +103,7 @@ and with the necessary code to create and initialize a suitable
## Syntax
This feature does not include any grammar modifications.
The grammar remains unchanged.
## Static Analysis
@ -116,40 +115,61 @@ from the declaration in the built-in class `Object`.
*Note that such a declaration cannot be a getter or setter, and it must
accept one positional argument of type `Invocation`, due to the
requirement that it must correctly override the declaration of
`noSuchMethod` in `Object`.*
`noSuchMethod` in the class `Object`. For instance, in addition to the
obvious choice `noSuchMethod(Invocation i)` it can be
`noSuchMethod(Object i, [String s])`, but not
`noSuchMethod(Invocation i, String s)`.*
*We introduce the notion of methods which are 'considered to be
implemented'. These methods are exactly the ones that we will generate
forwarders for, except that we keep the language slightly more abstract,
such that the ability to use a different implementation technique remains
open. During static analysis, "considering" these methods to be implemented
allows the enclosing class to be non-abstract, with no errors.*
If a non-abstract class _C_ has a non-trivial `noSuchMethod` then each
method signature (including getters and setters) which is a member of _C_'s
interface and for which _C_ does not have a non-abstract declaration is
_noSuchMethod forwarded_. No other situations give rise to a noSuchMethod
forwarded method signature.
If a non-abstract class _C_ has a non-trivial `noSuchMethod`, _C_ is
_considered to declare an implementation_ for each method, getter, and
setter which is a member of _C_'s interface, unless _C_ declares or
inherits an implementation of it.
*This means that whenever it is stated that a class has a noSuchMethod
forwarded method signature it is guaranteed to be a non-abstract class with
a non-trivial `noSuchMethod`.*
*Note that it is a compile-time error if a class _C_ has multiple
superinterfaces with a member named _m_, declared by declarations _D1
.. Dk_, and there is no declaration of _m_ in _C_, and there is no
declaration among _D1 .. Dk_ which is a correct override of every
declaration in _D1 .. Dk_. In other words, we ignore the situation where a
class is considered to implement a member _m_, but the signature of _m_ is
ambiguous, because it is based on a set of declarations that does not
contain a "most specific" element: That situation is an error, so we do not
need to handle it.*
If a class _C_ has a noSuchMethod forwarded signature then an implicit
method implementation implementing that method signature is induced in _C_.
In the case where _C_ already contains an abstract declaration with the
same name, the induced method implementation replaces the abstract
declaration.
It is a compile-time error if _C_ is considered to declare an
implementation of a method declaration _D_, and such an implementation
would override an inherited non-abstract declaration.
It is a compile-time error if a non-abstract class _C_ has a non-trivial
`noSuchMethod`, and a name `m` has a set of method signatures in the
superinterfaces of _C_ where none is most specific, and there is no
declaration in _C_ which provides such a most specific method signature.
*This can only happen if the given implementation satisfies some, but not
all requirements. In the example below, a `foo(int i)` implementation is
inherited and a superinterface declares `foo([int i])`. This is a
compile-time error because it would be error prone to generate a forwarder
in `C` which will silently override an implementation which "almost"
satisfies the requirement in the superinterface.*
*This means that even in the situation where everything else implies that a
noSuchMethod forwarder should be induced, signature ambiguities must still
be resolved by a developer-written declaration, it cannot be a consequence
of implicitly inducing a noSuchMethod forwarder. However, that
developer-written declaration could be an abstract method in the
non-abstract class itself.*
*Note that there is no most specific method signature if there are several
method signatures which are equally specific with respect to the argument
types and return type, but an optional formal parameter in these signatures
has different default values in different signatures.*
It is a compile-time error if a class _C_ has a noSuchMethod forwarded
method signature _S_ for a method named _m_, as well as an implementation
of _m_.
*This can only happen if that implementation is inherited and satisfies
some, but not all requirements of the noSuchMethod forwarded method
signature. In the example below, a `foo(int i)` implementation is inherited
and a superinterface declares `foo([int i])`. This is a compile-time error
because `C` does not have a method implementation with signature
`foo([int])`, but if one were to be implicitly induced it would override
`A.foo` (which is capable of accepting some but not all of the argument
lists that an implementation of `foo([int])` would allow). We have made
this an error because it would be error prone to induce a forwarder in `C`
which will silently override an `A.foo` which "almost" satisfies the
requirement in the superinterface. In particular, developers are likely to
be surprised if `A.foo` is not called even when it is passed a single
`int` argument, which precisely matches the declaration of `A.foo`.*
```dart
class A {
@ -166,98 +186,105 @@ class C extends A implements B {
}
```
*This error can be eliminated by adding a disambiguating abstract method
declaration to `C` for `foo`.*
*Note that this makes it a breaking change, in situations where such a
signature conflict exists in some subtype like `C`, to change an abstract
method declaration to a method implementation: If `A` had been an abstract
class and `A.foo` an abstract method which was replaced by an `A.foo`
declaration which implements the method, the error on `foo` in class `C`
would be introduced because `A.foo` was implemented. There is a reasonably
practical workaround, though: implement `C.foo` with a signature that
resolves the conflict. That implementation might invoke `A.foo` in a
superinvocation, or it might forward to `noSuchMethod`, or some times one
and some times the other, that is up to the developer who writes `C.foo`.*
```dart
// class A and B are unchanged from the previous example.
*Note that it is _not_ a compile-time error if the interface of _C_ has a
noSuchMethod forwarded method signature _S_ with name _m_, and a superclass
of _C_ also has a noSuchMethod forwarded method signature named _m_, such
that the implicitly induced implementation of the former overrides the
implicitly induced implementation of the latter. In other words, it is OK
for a generated forwarder to override another generated forwarder.*
class C extends A implements B {
noSuchMethod(Invocation i) => ...;
foo([int i]); // No ambiguity; will forward to `noSuchMethod`.
}
```
*Note that it is _not_ a compile-time error if _C_ is considered to declare an
implementation of a method declaration _D_, and such an implementation
would override an inherited declaration with the same name that some
superclass is considered to have. In other words, it is OK for a generated
forwarder to override another generated forwarder.*
*Note that when a class _C_ is considered to declare an implementation of a
given member, it allows superinvocations in subclasses.*
*Note that when a class _C_ has an implicitly induced implementation of a
method, superinvocations in subclasses are allowed, just like they would
have been for a developer-written implementation.*
```dart
abstract class D { baz(); }
class E implements D {}
class E implements D {
noSuchMethod(Invocation i) => null;
}
class F extends E { baz() { super.baz(); }} // OK
```
## Dynamic Semantics
Consider a program _P_ that contains a non-abstract class _C_ which has a
non-trivial `noSuchMethod`, and for which some methods, getters, or setters
_m1 .. mk_ are considered to be implemented, as defined in the previous
section.
Assume that a class _C_ has an implicitly induced implementation of a
method _m_ with positional formal parameters
_T<sub>1</sub> a<sub>1</sub>..., T<sub>k</sub> a<sub>k</sub>_
and named formal parameters
_T<sub>k+1</sub> n<sub>1</sub>..., T<sub>k+m</sub> n<sub>m</sub>_.
Said implementation will then create an instance _i_ of the predefined
class `Invocation` such that its
*This means that _m1 .. mk_ are present in the interface of _C_, but they
do not have an implementation, except for special cases like when the
implementation is a generated forwarder in a superclass of _C_ which
is not a correct override of the method in the interface of _C_.*
- `isGetter` evaluates to true iff _m_ is a getter,
`isSetter` evaluates to true iff _m_ is a setter,
`isMethod` evaluates to true iff _m_ is a method.
- `memberName` evaluates to the symbol for the name _m_.
- `positionalArguments` evaluates to an immutable list whose
values are _a<sub>1</sub>..., a<sub>k</sub>_.
- `namedArguments` evaluates to an immutable map with the same keys
and values as
_{n<sub>1</sub>: n<sub>1</sub>..., n<sub>m</sub>: n<sub>m</sub>}_
The semantics of _P_ is then such that it behaves as if _C_ had been
modified by adding declarations of _m1 .. mk_ with the signatures
declared in the interface of _C_, and with an implementation of each
member _mj_. That implementation will invoke `noSuchMethod` on `this`
with an `Invocation` as argument which specifies the bindings of the
formal parameters to the actual arguments, and indicates whether _mj_ is a
method, getter, or setter.
*Note that the number of named arguments can be zero, in which case some of
the positional parameters can be optional. We do not need to mention
optional positional arguments separately, because they receive the same
treatment as required parameters (which are of course always positional).*
Finally the induced method implementation will invoke `noSuchMethod` with
_i_ as the actual argument, and return the result obtained from there.
*This determines the dynamic semantics of implicitly induced methods: The
declared return type and the formal parameters, with type annotations and
default values, are uniquely determined by the noSuchMethod forwarded
method signatures, and invocation of an implicitly induced method has the
same semantics of invocation of other methods. In particular, dynamic type
checks are performed on the actual arguments upon invocation when the
corresponding formal parameter is covariant.*
*This ensures, relying on the heap soundness and expression soundness of
Dart (which ensures that every expression of type _T_ will evaluate to an
entity of type _T_), that all statically type safe invocations will invoke
regular method implementations, user-written or generated. In other words,
with statically checked calls there is no need for dynamic support for
`noSuchMethod` at all.*
*The generated forwarding methods behave in the same way as user-written
method declarations. For instance, dynamic type checks are performed on the
actual arguments when the corresponding formal parameter is covariant. A
generated forwarding method may have optional arguments with default
values. Given that there is always exactly one user-written signature which
is selected to be the signature of the forwarding method, these default
values are uniquely determined, and they work the same way as default
values do in a user-written method.*
a method implementation, user-written or implicitly induced. In other
words, with statically checked calls there is no need for dynamic support
for `noSuchMethod` at all.*
For a dynamic invocation of a member _m_ on a receiver _o_ that has a
non-trivial `noSuchMethod`, the semantics is such that an attempt to invoke
_m_ with the given actual arguments (including possibly some type
arguments) is made at first; if that fails (*because _o_ has no
arguments) is made at first. If that fails (*because _o_ has no
implementation of _m_ which can be invoked with the given argument list
shape, be it a regular method or a generated forwarder*) `noSuchMethod` is
invoked with an actual argument which is an `Invocation` describing the
actual arguments and invocation.
shape, be it a developer-written method or an implicitly induced
implementation*) `noSuchMethod` is invoked with an actual argument which is
an `Invocation` describing the actual arguments and invocation.
*This implies that dynamic invocations on receivers having a non-trivial
`noSuchMethod` will simply invoke the forwarders whenever possible.
Similarly, the "automatic" support for tearing off a method in the static
interface of the receiver which is not implemented, but supported via
`noSuchMethod` and a generated forwarder will still work for dynamic
invocations, as well as static ones.*
Similarly, it will work for dynamic invocations as well as statically
checked ones to tear off a method which is in the interface of the receiver
and implemented as a generated forwarder.*
*The only remaining situation is when a dynamic invocation invokes a method
which is not present in the static interface of the receiver, or when a
method with that name is present, but its signature does not allow for the
given invocation (e.g., because there are too few positional arguments).
In this situation, the regular instance method invocation has failed (there
is no such regular method, and no such generated forwarder). Such a dynamic
invocation must then dynamically determine whether the given receiver has a
non-trivial `noSuchMethod` and invoke it, rather than just invoking the
behavior of `noSuchMethod` in `Object` immediately (that is, throwing a
`NoSuchMethodError`). In this situation, `noSuchMethod` must also support
both method invocations and tear-offs, because there is no generated
forwarder to do that.*
given invocation (e.g., because some required arguments are omitted). In
this situation, the regular instance method invocation has failed (there is
no such regular method, and no such generated forwarder). Such a dynamic
invocation will then invoke `noSuchMethod`. In this situation, a
developer-written implementation of `noSuchMethod` should also support both
method invocations and tear-offs explicitly (as it should before this
feature was added), because there is no generated forwarder to do that.*
*This approach may incur a certain performance penalty, but only for these
invocations (which are dynamic, and have already failed to invoke an
@ -267,18 +294,17 @@ existing method, regular or generated).*
statically checked and dynamic invocations: Whenever an instance method is
invoked, and no such method exists, `noSuchMethod` will be invoked.*
*Note that this allows dynamic code to support types that have conflicting
signatures. For instance, it would be possible to create a class having a
non-trivial `noSuchMethod` that accepts dynamic invocations corresponding
to having both a getter `int get foo` and a method `int foo()`, even though
that could never be achieved for the actual interface of the class of an
instance. This will allow dynamic code to be more generic than typed code
could be, of course, at the expense of being forced to remain dynamically
typed as long as these conflicting interfaces are used together.*
## Updates
* Nov 27th 2017, version 0.5: Changed terminology to use 'implicitly
induced method implementations'. Helped achieving a major simplifaction
of the dynamic semantics.
* Nov 22nd 2017, version 0.4: Removed support for explicitly requesting
generated forwarder in conflict case. Improved the clarity of many
parts.
* Oct 5th 2017, version 0.3: Clarified that generated forwarders must
pass an `Invocation` to `noSuchMethod` which specifies the bindings
of formal arguments to actual arguments. Clarified the treatment of