blob: 6fca33544cbf57b696a959ed9737046e22cb2f6d [file] [log] [blame] [view]
## NoSuchMethod Forwarding
Author: eernst@
**Status**: Background material, normative language now in dartLangSpec.tex.
**Version**: 0.7 (2018-07-10)
**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, *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
a regular method invocation, which in turn invokes `noSuchMethod`.
## Motivation
In Dart 1.x, `noSuchMethod` will be invoked whenever an attempt is made to
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). 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
support a given method using `noSuchMethod`:
```dart
class Foo {
foo(x) {}
}
class MockFoo implements Foo {
// PS: Make sure that a tear-off of `_mockFoo` has the same type
// as a tear-off of `Foo.foo`.
_mockFoo(x) {
// ... implement mock behavior for `foo` here.
}
noSuchMethod(Invocation i) {
if (i.memberName == #foo) {
if (i.isMethod &&
i.positionalArguments.length == 1 &&
i.namedArguments.isEmpty) {
return _mockFoo(i.positionalArguments[0]);
} else if (i.isGetter) {
return _mockFoo;
}
}
return super.noSuchMethod(i);
}
}
```
The reason why the type of a tear-off of `_mockFoo` should be the same
as the type of a tear-off of `foo` is that the former should be able to
emulate the properties of the latter faithfully, including the response
it gives rise to when subjected to type tests, either explicitly or
implicitly.
Obviously, this is verbose, tedious, and difficult to maintain if the
claimed superinterfaces (`implements ...`) in the mock class introduce
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 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
class Foo {
foo(x) {}
}
class MockFoo implements Foo {
noSuchMethod(Invocation i) {
if (i.memberName == #foo) {
if (i.isMethod &&
i.positionalArguments.length == 1 &&
i.namedArguments.isEmpty) {
// ... implement mock behavior for `foo` here.
}
}
return super.noSuchMethod(i);
}
}
```
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`.
## Syntax
The grammar remains unchanged.
## Static Analysis
We say that a class _C_ _has a non-trivial_ `noSuchMethod` if _C_ declares
or inherits a concrete method named `noSuchMethod` which is distinct
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 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)`.*
If a concrete 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 concrete declaration is
_noSuchMethod forwarded_.
A concrete class _C_ that does _not_ have a non-trivial `noSuchMethod`
implements its interface (*it is a compile-time error not to do so*), but
there may exist superclasses of _C_ declared in other libraries whose
interfaces include some private methods for which _C_ has no concrete
declaration (*such members are by definition omitted from the interface of
_C_, because their names are inaccessible*). Similarly, even if a class _D_
does have a non-trivial `noSuchMethod`, there may exist abstract
declarations of private methods with inaccessible names in superclasses of
_D_ for which _D_ has no concrete declaration. In both of these situations,
such inaccessible private method signatures are _noSuchMethod forwarded_.
No other situations give rise to a noSuchMethod forwarded method
signature.
*This means that whenever it is stated that a class _C_ has a noSuchMethod
forwarded method signature, it is guaranteed to be a concrete class with a
non-trivial `noSuchMethod`, or the signature is guaranteed to be
inaccessible. In the former case, the developer expressed the intent to
obtain implementations of "missing methods" by having a non-trivial
`noSuchMethod` declaration, and in the latter case it is impossible to
write declarations in _C_ that implement the missing private methods, but
they will then be provided as generated forwarders.*
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 a concrete 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 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
concrete 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 {
foo(int i) => null;
}
abstract class B {
foo([int i]);
}
class C extends A implements B {
noSuchMethod(Invocation i) => ...;
// Error on `foo`: Forwarder would override `A.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`.*
*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.*
*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 {
noSuchMethod(Invocation i) => null;
}
class F extends E { baz() { super.baz(); }} // OK
```
## Dynamic Semantics
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
- `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>}_
*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
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
implementation of _m_ which can be invoked with the given argument list
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, 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 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
existing method, regular or generated).*
*In return, this approach enforces the following simple invariant, for both
statically checked and dynamic invocations: Whenever an instance method is
invoked, and no such method exists, `noSuchMethod` will be invoked.*
*One special case to be aware of is where a forwarder is torn off and then
invoked with an actual argument list which does not match the formal
parameter list. In that situation we will get an invocation of
`Object.noSuchMethod` rather than the `noSuchMethod` in the original
receiver, because this is an invocation of a function object (and they do
not override `noSuchMethod`):*
```dart
class A {
dynamic noSuchMethod(Invocation i) => null;
void foo();
}
main() {
A a = new A();
dynamic f = a.foo;
// Invokes `Object.noSuchMethod`, not `A.noSuchMethod`, so it throws.
f(42);
}
```
## Updates
* Jul 10th 2018, version 0.7: Added requirement to generate forwarders
for inaccessible private methods even in the case where there is no
non-trivial `noSuchMethod`.
* Mar 22nd 2018, version 0.6: Added example to illustrate the case where a
torn-off method invokes `Object.noSuchMethod`, not the one in the
receiver, because of a non-matching actual argument list.
* 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
default values for optional arguments.
* Sep 20th 2017, version 0.2: Many smaller adjustments, based on review
feedback.
* Sep 18th 2017, version 0.1: Created the first version of this document.