Also updated spec on the static and dynamic types of functions literals and static functions along with the function objects obtained from closurizations of them. Introduced a separate notion of what it means to be a correct override relation (where the old text used subtyping, which won't suffice, for several reasons). Introduced the notion of interfaces as a separate kind of entity that contains method signatures (again, a new kind of entity), thus clarifying exactly which pieces of information is available during static analysis of member accesses, e.g., instance method invocations. Introduced 'combined' interfaces; they are needed for `mixin` declarations, and we will specify them soon (so it should be OK to have them now, even though they are unused). Change-Id: I6347df49b1aa7a81d74e25904ee75c19e8ac6930 Reviewed-on: https://dart-review.googlesource.com/c/81263 Reviewed-by: Leaf Petersen <leafp@google.com>
13 KiB
Feature Specification: Interface Conflict Management
Owner: eernst@
Status: Background material, normative text is now in dartLangSpec.tex. Note that the rules have changed, which means that this document cannot be used as a reference, it can only be used to get an overview of the ideas; please refer to the language specification for all technical details.
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, we have the following:
However, if the above rules would cause multiple members m1, ..., mk 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 typedynamic
, named parameters s of typedynamic
and return typedynamic
.
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 thanvoid
. - The built-in type
dynamic
is more interface-specific thanvoid
. - None of
Object
anddynamic
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
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<T1 .. Tk> respectively D<S1 .. Sk> such that there is a j in 1 .. k where Tj and Sj 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 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 D1 .. Dn denote this set of declarations.
It is a compile-time error if some declarations among D1 .. Dn are getters and others are non-getters.
Otherwise, if all of D1 .. Dn are getter declarations, the interface of C inherits one, Dj, whose return type is more interface-specific than that of every declaration in D1 .. Dn. It is a compile-time error if no such Dj 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, Dj, where its function type is more interface-specific than that of all declarations in D1 .. Dn. It is a compile-time error if no such declaration Dj 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 Di and Dj, if Ui is the return type from Di and Uj is the return type from Dj then Ui is more interface-specific than Uj 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 D1 .. Dn the interface of the class C will inherit, if we have multiple possible choices, let Di and Dj be such a pair of possible choices. It is a compile-time error if Di and Dj declare two optional formal parameters p1 and p2 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:
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:
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:
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 overrideObject 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.