Change-Id: I7a29b83a69f43cb695b4305442808fa45b745faa Reviewed-on: https://dart-review.googlesource.com/c/77440 Reviewed-by: Lasse R.H. Nielsen <lrn@google.com> Reviewed-by: Leaf Petersen <leafp@google.com>
16 KiB
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
:
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:
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
.
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.
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
T1 a1..., Tk ak
and named formal parameters
Tk+1 n1..., Tk+m nm.
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 a1..., ak.namedArguments
evaluates to an immutable map with the same keys and values as {n1: n1..., nm: nm}
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
):
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
tonoSuchMethod
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.