mirror of
https://github.com/dart-lang/sdk
synced 2024-10-14 12:30:03 +00:00
Add initial version of feature specification on interface conflicts.
This feature specification is intended to resolve bullet item 2 in the issue https://github.com/dart-lang/sdk/issues/31228. A rendered version corresponding to patch set 16 is available at https://gist.github.com/eernstg/a55c8000610a506bf0ca70b028d9f1eb. Change-Id: I7d3e67bd7dd2d2cfc73fbd491bcfbea1814421e0 Reviewed-on: https://dart-review.googlesource.com/c/40080 Reviewed-by: Lasse R.H. Nielsen <lrn@google.com>
This commit is contained in:
parent
f2bffc6872
commit
7c7df7bfa4
275
docs/language/informal/interface-conflicts.md
Normal file
275
docs/language/informal/interface-conflicts.md
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
# Feature Specification: Interface Conflict Management
|
||||||
|
|
||||||
|
**Owner**: eernst@
|
||||||
|
|
||||||
|
**Status**: Under discussion.
|
||||||
|
|
||||||
|
**Version**: 0.3 (2018-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
This document is a Dart 2 feature specification which specifies how to
|
||||||
|
handle conflicts among certain program elements associated with the
|
||||||
|
interface of a class. In particular, it specifies that multiple occurrences
|
||||||
|
of the same generic class in the superinterface hierarchy must receive the
|
||||||
|
same type arguments, and that no attempts are made at synthesizing a
|
||||||
|
suitable method signature if multiple distinct signatures are provided by
|
||||||
|
the superinterfaces, and none of them resolves the conflict.
|
||||||
|
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
In Dart 1, the management of conflicts during the computation of the
|
||||||
|
interface of a class is rather forgiving. On page 42 of
|
||||||
|
[ECMA-408](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-408.pdf),
|
||||||
|
we have the following:
|
||||||
|
|
||||||
|
> However, if the above rules would cause multiple members
|
||||||
|
> _m<sub>1</sub>, ..., m<sub>k</sub>_
|
||||||
|
> with the same name _n_ to be inherited (because identically named
|
||||||
|
> members existed in several superinterfaces) then at most one member
|
||||||
|
> is inherited.
|
||||||
|
>
|
||||||
|
> ...
|
||||||
|
>
|
||||||
|
> Then _I_ has a method named _n_, with _r_ required parameters of type
|
||||||
|
> `dynamic`, _h_ positional parameters of type `dynamic`, named parameters
|
||||||
|
> _s_ of type `dynamic` and return type `dynamic`.
|
||||||
|
|
||||||
|
In particular, the resulting class interface may then contain a method
|
||||||
|
signature which has been synthesized during static analysis, and which
|
||||||
|
differs from all declarations of the given method in the source code.
|
||||||
|
In the case where some superintenfaces specify some optional positional
|
||||||
|
parameters and others specify some named parameters, any attempt to
|
||||||
|
implement the synthesized method signature other than via a user-defined
|
||||||
|
`noSuchMethod` would fail (it would be a syntax error to declare both
|
||||||
|
kinds of parameters in the same method declaration).
|
||||||
|
|
||||||
|
For Dart 2 we modify this approach such that more emphasis is given to
|
||||||
|
predictability, and less emphasis is given to convenience: No class
|
||||||
|
interface will ever contain a method signature which has been
|
||||||
|
synthesized during static analysis, it will always be one of the method
|
||||||
|
interfaces that occur in the source code. In case of a conflict, the
|
||||||
|
developer must explicitly specify how to resolve the conflict.
|
||||||
|
|
||||||
|
To reinforce the same emphasis on predictability, we also specify that
|
||||||
|
it is a compile-time error for a class to have two superinterfaces which
|
||||||
|
are instantiations of the same generic class with different type arguments.
|
||||||
|
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
The grammar remains unchanged.
|
||||||
|
|
||||||
|
|
||||||
|
## Static Analysis
|
||||||
|
|
||||||
|
We introduce a new relation among types, _more interface-specific than_,
|
||||||
|
which is similar to the subtype relation, but which treats top types
|
||||||
|
differently.
|
||||||
|
|
||||||
|
- The built-in class `Object` is more interface-specific than `void`.
|
||||||
|
- The built-in type `dynamic` is more interface-specific than `void`.
|
||||||
|
- None of `Object` and `dynamic` is more interface-specific than the other.
|
||||||
|
- All other subtype rules are also valid rules about being more
|
||||||
|
interface-specific.
|
||||||
|
|
||||||
|
This means that we will express the complete rules for being 'more
|
||||||
|
interface-specific than' as a slight modification of
|
||||||
|
[subtyping.md](https://github.com/dart-lang/sdk/blob/master/docs/language/informal/subtyping.md)
|
||||||
|
and in particular, the rule 'Right Top' will need to be split in cases
|
||||||
|
such that `Object` and `dynamic` are more interface-specific than `void` and
|
||||||
|
mutually unrelated, and all other types are more interface-specific than
|
||||||
|
both `Object` and `dynamic`.
|
||||||
|
|
||||||
|
*For example, `List<Object>` is more interface-specific than `List<void>`
|
||||||
|
and incomparable to `List<dynamic>`; similarly, `int Function(void)` is
|
||||||
|
more interface-specific than `void Function(Object)`, but the latter is
|
||||||
|
incomparable to `void Function(dynamic)`.*
|
||||||
|
|
||||||
|
It is a compile-time error if a class _C_ has two superinterfaces of the
|
||||||
|
form _D<T<sub>1</sub> .. T<sub>k</sub>>_ respectively
|
||||||
|
_D<S<sub>1</sub> .. S<sub>k</sub>>_ such that there is a _j_ in _1 .. k_
|
||||||
|
where _T<sub>j</sub>_ and _S<sub>j</sub>_ denote types that are not
|
||||||
|
mutually more interface-specific than each other.
|
||||||
|
|
||||||
|
*This means that the (direct and indirect) superinterfaces must agree on
|
||||||
|
the type arguments passed to any given generic class. Note that the case
|
||||||
|
where the number of type arguments differ is unimportant because at least
|
||||||
|
one of them is already a compile-time error for other reasons. Also note
|
||||||
|
that it is not sufficient that the type arguments to a given superinterface
|
||||||
|
are mutual subtypes (say, if `C` implements both `I<dynamic>` and
|
||||||
|
`I<Object>`), because that gives rise to ambiguities which are considered
|
||||||
|
to be compile-time errors if they had been created in a different way.*
|
||||||
|
|
||||||
|
This compile-time error also arises if the type arguments are not given
|
||||||
|
explicitly.
|
||||||
|
|
||||||
|
*They might be obtained via
|
||||||
|
[instantiate-to-bound](https://github.com/dart-lang/sdk/blob/master/docs/language/informal/instantiate-to-bound.md)
|
||||||
|
or, in case such a mechanism is introduced, they might be inferred.*
|
||||||
|
|
||||||
|
*The language specification already contains verbiage to this effect, but we
|
||||||
|
mention it here for two reasons: First, it is a recent change which has been
|
||||||
|
discussed in the language team together with the rest of the topics in this
|
||||||
|
document because of their similar nature and motivation. Second, we note
|
||||||
|
that this restriction may be lifted in the future. It was a change in the
|
||||||
|
specification which did not break many existing programs because `dart2js`
|
||||||
|
always enforced that restriction (even though it was not specified in the
|
||||||
|
language specification), so in that sense it just made the actual situation
|
||||||
|
explicit. However, it may be possible to lift the restriction: Given that an
|
||||||
|
instance of a class that has `List<int>` among its superinterfaces can be
|
||||||
|
accessed via a variable of type `List<num>`, it seems unlikely that it would
|
||||||
|
violate any language invariants to allow the class of that instance to have
|
||||||
|
both `List<int>` and `List<num>` among its superinterfaces. We may then
|
||||||
|
relax the rule to specify that for each generic class _G_ which occurs among
|
||||||
|
superinterfaces, there must be a unique superinterface which is the most
|
||||||
|
specific instantiation of _G_.*
|
||||||
|
|
||||||
|
During computation of the interface of a class _C_, it may be the case that
|
||||||
|
multiple direct superinterfaces have a declaration of a member of the same
|
||||||
|
name _n_, and class _C_ does not declare member named _n_.
|
||||||
|
Let _D<sub>1</sub> .. D<sub>n</sub>_ denote this set of declarations.
|
||||||
|
|
||||||
|
It is a compile-time error if some declarations among
|
||||||
|
_D<sub>1</sub> .. D<sub>n</sub>_ are getters and others are non-getters.
|
||||||
|
|
||||||
|
Otherwise, if all of _D<sub>1</sub> .. D<sub>n</sub>_ are getter
|
||||||
|
declarations, the interface of _C_ inherits one, _D<sub>j</sub>_, whose
|
||||||
|
return type is more interface-specific than that of every declaration in
|
||||||
|
_D<sub>1</sub> .. D<sub>n</sub>_. It is a compile-time error if no such
|
||||||
|
_D<sub>j</sub>_ exists.
|
||||||
|
|
||||||
|
*For example, it is an error to have two declarations with the signatures
|
||||||
|
`Object get foo` and `dynamic get foo`, and no others, because none of
|
||||||
|
these is more interface-specific than the other. This example illustrates
|
||||||
|
why it is unsatisfactory to rely on subtyping alone: If we had accepted
|
||||||
|
this kind of ambiguity then it would be difficult to justify the treatment
|
||||||
|
of `o.foo.bar` during static analysis where `o` has type _C_: If it is
|
||||||
|
considered to be a compile-time error then `dynamic get foo` is being
|
||||||
|
ignored, and if it is not an error then `Object get foo` is being ignored,
|
||||||
|
and each of these behaviors may be surprising and/or error-prone. Hence, we
|
||||||
|
require such a conflict to be resolved explicitly, which may be done by
|
||||||
|
writing a signature in the class which overrides both method signatures
|
||||||
|
from the superinterfaces and explicitly chooses `Object` or `dynamic`.*
|
||||||
|
|
||||||
|
Otherwise, (*when all declarations are non-getter declarations*), the
|
||||||
|
interface of _C_ inherits one, _D<sub>j</sub>_, where its function type is
|
||||||
|
more interface-specific than that of all declarations in
|
||||||
|
_D<sub>1</sub> .. D<sub>n</sub>_. It is a compile-time error if no such
|
||||||
|
declaration _D<sub>j</sub>_ exists.
|
||||||
|
|
||||||
|
*In the case where more than one such declaration exists, it is known that
|
||||||
|
their parameter list shapes are identical, and their return types and
|
||||||
|
parameter types are pairwise mutually more interface-specific than each
|
||||||
|
other (i.e., for any two such declarations _D<sub>i</sub>_ and _D<sub>j</sub>_,
|
||||||
|
if _U<sub>i</sub>_ is the return type from _D<sub>i</sub>_ and
|
||||||
|
_U<sub>j</sub>_ is the return type from _D<sub>j</sub>_ then
|
||||||
|
_U<sub>i</sub>_ is more interface-specific than _U<sub>j</sub>_ and
|
||||||
|
vice versa, and similarly for each parameter type). This still allows for
|
||||||
|
some differences. We ignore differences in metadata on formal parameters
|
||||||
|
(we do not consider method signatures in interfaces to have metadata). But
|
||||||
|
we need to consider one more thing:*
|
||||||
|
|
||||||
|
In this decision about which declaration among
|
||||||
|
_D<sub>1</sub> .. D<sub>n</sub>_
|
||||||
|
the interface of the class _C_ will inherit, if we have multiple possible
|
||||||
|
choices, let _D<sub>i</sub>_ and _D<sub>j</sub>_ be such a pair of possible
|
||||||
|
choices. It is a compile-time error if _D<sub>i</sub>_ and _D<sub>j</sub>_
|
||||||
|
declare two optional formal parameters _p<sub>1</sub>_ and _p<sub>2</sub>_
|
||||||
|
such that they correspond to each other (*same name if named, or else same
|
||||||
|
position*) and they specify different default values.
|
||||||
|
|
||||||
|
|
||||||
|
## Discussion
|
||||||
|
|
||||||
|
Conflicts among distinct top types may be considered to be spurious in the
|
||||||
|
case where said type occurs in a contravariant position in the method
|
||||||
|
signature. Consider the following example:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
abstract class I1 {
|
||||||
|
void foo(dynamic d);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class I2 {
|
||||||
|
void foo(Object o);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class C implements I1, I2 {}
|
||||||
|
```
|
||||||
|
|
||||||
|
In both situations—when `foo` accepts an argument of type `dynamic`
|
||||||
|
and when it accepts an `Object`—the acceptable actual arguments are
|
||||||
|
exactly the same: _Every_ object can be passed. Moreover, the formal
|
||||||
|
parameters `d` and `o` are not in scope anywhere, so there will never be
|
||||||
|
an expression like `d.bar` or `o.bar` which is allowed respectively
|
||||||
|
rejected because the receiver is or is not `dynamic`. In other words,
|
||||||
|
_it does not matter_ for clients of `C` whether that argument type is
|
||||||
|
`dynamic` or `Object`.
|
||||||
|
|
||||||
|
During inference, the type-from-context for an actual argument to `foo`
|
||||||
|
will depend on the choice: It will be `dynamic` respectively `Object`.
|
||||||
|
However, this choice will not affect the treatment of the actual
|
||||||
|
argument.
|
||||||
|
|
||||||
|
One case worth considering is the following:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
abstract class I1 {
|
||||||
|
void foo(dynamic f());
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class I2 {
|
||||||
|
void foo(Object f());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If a function literal is passed in at a call site, it may have its return
|
||||||
|
type inferred to `dynamic` respectively `Object`. This will change the
|
||||||
|
type-from-context for any returned expressions, but just like the case
|
||||||
|
for the actual parameter, that will not change the treatment of such
|
||||||
|
expressions. Again, it does not matter for clients calling `foo` whether
|
||||||
|
that type is `dynamic` or `Object`.
|
||||||
|
|
||||||
|
Conversely, the choice of top type matters when it is placed in a
|
||||||
|
contravariant location in the parameter type:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
abstract class I1 {
|
||||||
|
void foo(int f(dynamic d));
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class I2 {
|
||||||
|
void foo(int f(Object o));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this situation, a function literal used as an actual argument at a call
|
||||||
|
site for `foo` would receive an inferred type annotation for its formal
|
||||||
|
parameter of `dynamic` respectively `Object`, and the usage of that parameter
|
||||||
|
in the body of the function literal would then differ. In other words, the
|
||||||
|
developer who declares `foo` may decide whether the code in the body of the
|
||||||
|
function literal at the call sites should use strict or relaxed type
|
||||||
|
checking—and it would be highly error-prone if this decision were
|
||||||
|
to be made in a way which is unspecified.
|
||||||
|
|
||||||
|
All in all, it may be useful to "erase" all top types to `Object` when they
|
||||||
|
occur in contravariant positions in method signatures, such that the
|
||||||
|
differences that may exist do not create conflicts; in contrast, the top
|
||||||
|
types that occur in covariant positions are significant, and hence the fact
|
||||||
|
that we require such conflicts to be resolved explicitly is unlikely to be
|
||||||
|
relaxed.
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
* Apr 24th 2018, version 0.3: Renamed 'override-specific' to
|
||||||
|
'interface-specific', to avoid giving the impression that it can be
|
||||||
|
used to determine whether a given signature can override another one
|
||||||
|
(the override check must use different rules, e.g., it must allow
|
||||||
|
`dynamic foo();` to override `Object foo();` _and_ vice versa).
|
||||||
|
|
||||||
|
* Apr 16th 2018, version 0.2: Introduced the relation 'more
|
||||||
|
override-specific than' in order to handle top types more consistently
|
||||||
|
and concisely.
|
||||||
|
|
||||||
|
* Feb 8th 2018, version 0.1: Initial version.
|
Loading…
Reference in a new issue