| # Typing of members of dynamic |
| |
| **Author**: eernst@. |
| |
| **Version**: 0.2 (2018-09-04) |
| |
| **Status**: Background material. Normative text is now in dartLangSpec.tex. |
| |
| **This document** is a Dart 2 feature specification of the static typing |
| of instance members of a receiver whose static type is `dynamic`. |
| |
| This document uses discussions in |
| [this github issue](https://github.com/dart-lang/sdk/issues/32414) |
| as a starting point. |
| |
| |
| ## Motivation |
| |
| For Dart programs using a statically typed style, it is often helpful to |
| use the most precise static type for an expression which is still sound. |
| In contrast, if such an expression gets type `dynamic` it often causes |
| subsequent type computations such as inference to make less useful |
| decisions, or it may mask errors which are likely or guaranteed to occur at |
| run time. Here is an example: |
| |
| ```dart |
| class A { |
| String toString([bool b = true]) => |
| b ? 'This is an A!' : 'Whatever'; |
| } |
| |
| foo(List<String> xs) { |
| for (String s in xs) print(s); |
| } |
| |
| main() { |
| dynamic d = new A(); |
| var xs = [d.toString()]; |
| foo(xs); |
| } |
| ``` |
| |
| In this example, the actual type argument passed to the list literal |
| `[d.toString()]` by inference depends on the static type of the expression |
| `d.toString()`. If that expression is given the type `dynamic` (as it would |
| be in Dart 1) then the resulting list will be a `List<dynamic>`, and hence |
| the invocation of `foo` would fail because it requires an argument of type |
| `List<String>`. |
| |
| In general, a receiver with static type `dynamic` is assumed to have all |
| members, i.e., we can make the attempt to invoke a getter, setter, method, |
| or operator with any name, and we can pass any list of actual arguments and |
| possibly some type arguments, and that will not cause any compile-time |
| errors. Various checks may be performed at run time when such an invocation |
| takes place, and that is the whole point: Usage of expressions of type |
| `dynamic` allows developers to skip the static checks and instead have |
| dynamic checks. |
| |
| However, every object in a Dart program execution has a type which is a |
| subtype of `Object`. Hence, for each member declared by `Object`, it will |
| either inherit an implementation declared by `Object`, or it will have some |
| implementation specified as an override for the declaration in |
| `Object`. Given that overriding declarations must satisfy certain |
| constraints, we do know something about the properties of a member declared |
| in `Object`. This allows static analysis to give static types to some |
| expressions which are more precise than `dynamic`, even for a member access |
| where the receiver has type `dynamic`, and that is the topic of this |
| document. |
| |
| We will obey the general principle that an instance method invocation |
| (including getters, setters, and operators) which would be compiled without |
| errors under some typing of the receiver must also be without compile-time |
| errors when the receiver has type `dynamic`. It should be noted that there |
| is no requirement that the typing relies only on declarations which are in |
| scope at the point where the invocation occurs, it must instead be possible |
| to _declare_ such a class that the invocation can be statically typed. The |
| point in obeying this principle is that dynamic invocation should be |
| capable of performing _every_ invocation which is possible using types. |
| |
| For instance, `d.toString(42)` cannot have a compile-time error when `d` |
| has static type `dynamic`, because we could have the following declaration, |
| and `d` could have had type `D`: |
| |
| ```dart |
| class D { |
| noSuchMethod(Object o) => o; |
| Null toString([int i]) => null; |
| } |
| ``` |
| |
| Similarly, `d.noSuchMethod('Oh!')` would not be a compile-time error, |
| because a contravariant type annotation on the parameter as shown above |
| would allow actual arguments of other types than `Invocation`. |
| |
| On the other hand, it is safe to assign the static type `String` to |
| `d.toString()`, because that invocation will definitely invoke the |
| implementation of `toString` in `Object` or an override thereof, and that |
| override must have a return type which is `String` or a subtype (for |
| `String` that can only be `Null`, but in general it can be any subtype). |
| |
| It may look like a highly marginal corner of the language to give special |
| treatment to the few methods declared in `Object`, but it does matter in |
| practice that a number of invocations of `toString` are given the type |
| `String`. Other members like `hashCode` get the same treatment in order to |
| have a certain amount of consistency. |
| |
| Moreover, we have considered generalizing the notion of "the type dynamic" |
| such that it becomes "the type dynamic based on `T`" for any given type |
| `T`, using some syntax, e.g., `dynamic(T)`. The idea would be that |
| statically known methods invoked on a receiver of type `dynamic(T)` would |
| receive static checking, but invocations of other methods get dynamic |
| checking. With that, the treatment specified in this document (which was |
| originally motivated by the typing of `toString`) will suddenly apply to |
| any member declared by `T`, where `T` can be any type (that is, any |
| declarable member). It is then important to have a systematic approach and |
| a simple conceptual "story" about how it works, and why it works like |
| that. This document should be a usable starting point for such an approach |
| and story. |
| |
| |
| ## Static Analysis |
| |
| In this section, `Object` denotes the built-in class `Object`, and |
| `dynamic` denotes the built-in type `dynamic`. |
| |
| Let `e` be an expression of the form `d.m`, which is not followed by an |
| argument part, where the static type of `d` is `dynamic`, and `m` is a |
| getter declared in `Object`; if the return type of `Object.m` is `T` then |
| the static type of `e` is `T`. |
| |
| *For instance, `d.hashCode` has type `int` and `d.runtimeType` has type |
| `Type`.* |
| |
| Let `e` be an expression of the form `d.m`, which is not followed by an |
| argument part, where the static type of `d` is `dynamic`, and `m` is a |
| method declared in `Object` whose method signature has type `F` (*which is |
| a function type*). The static type of `e` is then `F`. |
| |
| *For instance, `d.toString` has type `String Function()`.* |
| |
| Let `e` be an expression of the form `d.m(arguments)` or |
| `d.m<typeArguments>(arguments)` where the static type of `d` is `dynamic`, |
| `m` is a getter declared in `Object` with return type `F`, `arguments` is |
| an actual argument list, and `typeArguments` is a list of actual type |
| arguments, if present. Static analysis will then process `e` as a function |
| expression invocation where a function of static type `F` is applied to the |
| given argument part. |
| |
| *So `d.runtimeType(42)` is a compile-time error, because it is checked as a |
| function expression invocation where an entity of static type `Type` is |
| invoked. Note that it could actually succeed: An overriding implementation |
| of `runtimeType` could return an instance whose dynamic type is a subtype |
| of `Type` that has a `call` method. We decided to make it an error because |
| it is likely to be a mistake, especially in cases like `d.hashCode()` where |
| a developer might have forgotten that `hashCode` is a getter.* |
| |
| Let `e` be an expression of the form `d.m(arguments)` where the static type |
| of `d` is `dynamic`, `arguments` is an actual argument list, and `m` is a |
| method declared in `Object` whose method signature has type `F`. If the |
| number of positional actual arguments in `arguments` is less than the |
| number of required positional arguments of `F` or greater than the number |
| of positional arguments in `F`, or if `arguments` includes any named |
| arguments with a name that is not declared in `F`, the type of `e` is |
| `dynamic`. Otherwise, the type of `e` is the return type in `F`. |
| |
| *So `d.toString(bazzle: 42)` has type `dynamic` whereas `d.toString()` has |
| type `String`. Note that invocations which "do not fit" the statically |
| known declaration are not errors, they just get return type `dynamic`.* |
| |
| Let `e` be an expression of the form `d.m<typeArguments>(arguments)` where |
| the static type of `d` is `dynamic`, `typeArguments` is a list of actual |
| type arguments, `arguments` is an actual argument list. It is a |
| compile-time error if `m` is a non-generic method declared in `Object`. |
| |
| *No generic methods are declared in `Object`. Hence, we do not specify that |
| there must be the statically required number of actual type arguments, and |
| they must satisfy the bounds. That would otherwise be the consistent |
| approach, because the invocation is guaranteed to fail when any of those |
| requirements are violated, but generalizations of this mechanism would need |
| to include such rules.* |
| |
| For an instance method invocation `e` (including invocations of getters, |
| setters, and operators) where the receiver has static type `dynamic` and |
| `e` does not match any of the above cases, the static type of `e` is |
| `dynamic`. |
| |
| When a `cascadeSection` performs a getter or method invocation that |
| corresponds to one of the cases above, the corresponding static analysis |
| and compile-time errors apply. |
| |
| *For instance, `d..foobar(16)..hashCode()` is an error.* |
| |
| *Note that only very few forms of instance method invocation with a |
| receiver of type `dynamic` can be a compile-time error. Of course, |
| some expressions like `x[1, 2]` are syntax errors even though they |
| could also be considered "invocations", and subexpressions are checked |
| separately so any given actual argument could be a compile-time |
| error. But almost any given argument list shape could be handled via |
| `noSuchMethod`, and an argument of any type could be accepted because any |
| formal parameter in an overriding declaration could have its type |
| annotation contravariantly changed to `Object`. So it is a natural |
| consequence of the principle mentioned in 'Motivation' that a `dynamic` |
| receiver admits almost all instance method invocations. The few cases where |
| an instance method invocation with a receiver of type `dynamic` is an error |
| are either guaranteed to fail at run time, or they are very likely to be |
| developer mistakes.* |
| |
| |
| ## Dynamic Semantics |
| |
| This feature has no implications for the dynamic semantics, beyond the ones |
| which are derived directly from the static typing. |
| |
| *For instance, a list literal may have a run-time type which is determined |
| via inference by the static type of its elements, as in the example in the |
| 'Motivation' section, or the actual type argument may be influenced by the |
| typing context, which may again depend on the rules specified in this |
| document.* |
| |
| |
| ## Revisions |
| |
| - 0.2 (2018-09-04) Adjustment to make `d.hashCode()` and similar |
| expressions an error, cf. |
| [this github issue](https://github.com/dart-lang/sdk/issues/34320). |
| |
| - 0.1 (2018-03-13) Initial version, based on discussions in |
| [this github issue](https://github.com/dart-lang/sdk/issues/32414). |