From a6d422957ddc2b7e62ddafeae73165aae9229694 Mon Sep 17 00:00:00 2001 From: Erik Ernst Date: Fri, 1 Dec 2017 12:39:19 +0000 Subject: [PATCH] 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 Reviewed-by: Leaf Petersen --- .../informal/nosuchmethod-forwarding.md | 258 ++++++++++-------- 1 file changed, 142 insertions(+), 116 deletions(-) diff --git a/docs/language/informal/nosuchmethod-forwarding.md b/docs/language/informal/nosuchmethod-forwarding.md index 60cc2f6f21e..8feaf6889f7 100644 --- a/docs/language/informal/nosuchmethod-forwarding.md +++ b/docs/language/informal/nosuchmethod-forwarding.md @@ -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 +_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 -*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 _a1..., ak_. +- `namedArguments` evaluates to an immutable map with the same keys + and values as + _{n1: n1..., nm: nm}_ -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