## 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 _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`):* ```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.