2023.10.27
- CHANGE Update UP to handle intersection types earlier. This
prevents UP from producing results of the form
(X & B)?
.
2020.09.01
- CHANGE Update UP in cases about type variables and promoted type variables involving F-bounds, and in two cases about function types.
2020.08.13
- Move helper predicates to the null safety specification.
2020.07.21
- CHANGE Specify treatment of mixed hierarchies.
2020.03.30
- CHANGE Update DOWN algorithm with extensions for FutureOr
This documents the currently implemented upper and lower bound computation, modified to account for explicit nullability and the accompanying type system changes (including the legacy types). In the interest of backwards compatibility, it does not try to fix the various issues with the existing algorithm.
The syntactic set of types used in this draft are a slight simplification of full Dart types, as described in the subtyping document here
For convenience, we generally write function types with all named parameters in an unspecified canonical order, and similarly for the named fields of record types. In all cases unless otherwise specifically called out, order of named parameters and fields is semantically irrelevant: any two types with the same named parameters (named fields, respectively) are considered the same type.
The predicates here are defined as algorithms and should be read from top to bottom. That is, a case in a predicate is considered to match only if none of the cases above it have matched.
We assume that type variables have been alpha-varied as needed.
We assume that type aliases have been expanded, and that all types are named (prefixed) canonically.
This document relies on several type classification helper predicates
which are specified in the
null safety specification:
TOP, which is true for all top types;
OBJECT, which is true for types equivalent to Object
;
BOTTOM, which is true for types equivalent to Never
;
NULL, which is true for types equivalent to Null
;
MORETOP, which is a total order on top and Object
types; and
MOREBOTTOM, which is an (almost) total order on bottom and Null
types.
Note that this algorithm is defined for types, not type schemas. For the upper bound of two type schemas, see the inference specification.
We define the upper bound of two types T1 and T2 to be UP(T1
,T2
) as follows.
-
UP(
T
,T
) =T
-
UP(
T1
,T2
) where TOP(T1
) and TOP(T2
) =T1
if MORETOP(T1
,T2
)T2
otherwise
-
UP(
T1
,T2
) =T1
if TOP(T1
) -
UP(
T1
,T2
) =T2
if TOP(T2
) -
UP(
T1
,T2
) where BOTTOM(T1
) and BOTTOM(T2
) =T2
if MOREBOTTOM(T1
,T2
)T1
otherwise
-
UP(
T1
,T2
) =T2
if BOTTOM(T1
) -
UP(
T1
,T2
) =T1
if BOTTOM(T2
) -
UP(
X1 & B1
,T2
) =T2
ifX1 <: T2
- otherwise
X1
ifT2 <: X1
- otherwise UP(
B1a
,T2
) whereB1a
is the greatest closure ofB1
with respect toX1
, as defined in inference.md.
-
UP(
T1
,X2 & B2
) =X2
ifT1 <: X2
- otherwise
T1
ifX2 <: T1
- otherwise UP(
T1
,B2a
) whereB2a
is the greatest closure ofB2
with respect toX2
, as defined in inference.md.
-
UP(
T1
,T2
) where NULL(T1
) and NULL(T2
) =T2
if MOREBOTTOM(T1
,T2
)T1
otherwise
-
UP(
T1
,T2
) where NULL(T1
) =T2
ifT2
is nullableT2*
ifNull <: T2
orT1 <: Object
(that is,T1
orT2
is legacy)T2?
otherwise
-
UP(
T1
,T2
) where NULL(T2
) =T1
ifT1
is nullableT1*
ifNull <: T1
orT2 <: Object
(that is,T1
orT2
is legacy)T1?
otherwise
-
UP(
T1
,T2
) where OBJECT(T1
) and OBJECT(T2
) =T1
if MORETOP(T1
,T2
)T2
otherwise
-
UP(
T1
,T2
) where OBJECT(T1
) =T1
ifT2
is non-nullableT1*
ifNull <: T2
(that is,T2
is legacy)T1?
otherwise
-
UP(
T1
,T2
) where OBJECT(T2
) =T2
ifT1
is non-nullableT2*
ifNull <: T1
(that is,T1
is legacy)T2?
otherwise
-
UP(
T1*
,T2*
) =S*
whereS
is UP(T1
,T2
) -
UP(
T1*
,T2?
) =S?
whereS
is UP(T1
,T2
) -
UP(
T1?
,T2*
) =S?
whereS
is UP(T1
,T2
) -
UP(
T1*
,T2
) =S*
whereS
is UP(T1
,T2
) -
UP(
T1
,T2*
) =S*
whereS
is UP(T1
,T2
) -
UP(
T1?
,T2?
) =S?
whereS
is UP(T1
,T2
) -
UP(
T1?
,T2
) =S?
whereS
is UP(T1
,T2
) -
UP(
T1
,T2?
) =S?
whereS
is UP(T1
,T2
) -
UP(
X1 extends B1
,T2
) =T2
ifX1 <: T2
- otherwise
X1
ifT2 <: X1
- otherwise UP(
B1a
,T2
) whereB1a
is the greatest closure ofB1
with respect toX1
, as defined in inference.md.
-
UP(
T1
,X2 extends B2
) =X2
ifT1 <: X2
- otherwise
T1
ifX2 <: T1
- otherwise UP(
T1
,B2a
) whereB2a
is the greatest closure ofB2
with respect toX2
, as defined in inference.md.
-
UP(
T Function<...>(...)
,Function
) =Function
-
UP(
Function
,T Function<...>(...)
) =Function
-
UP(
T0 Function<X0 extends B00, ... Xm extends B0m>(P00, ... P0k)
,T1 Function<X0 extends B10, ... Xm extends B1m>(P10, ... P1l)
) =R0 Function<X0 extends B20, ..., Xm extends B2m>(P20, ..., P2q)
if:- each
B0i
andB1i
are equal types (syntactically) - Both have the same number of required positional parameters
q
is min(k
,l
)R0
is UP(T0
,T1
)B2i
isB0i
P2i
is DOWN(P0i
,P1i
)
- each
-
UP(
T0 Function<X0 extends B00, ... Xm extends B0m>(P00, ... P0k, Named0)
,T1 Function<X0 extends B10, ... Xm extends B1m>(P10, ... P1k, Named1)
) =R0 Function<X0 extends B20, ..., Xm extends B2m>(P20, ..., P2k, Named2)
if:- each
B0i
andB1i
are equal types (syntactically) - All positional parameters are required
Named0
contains an entry (optional or required) of the formR0i xi
for every required named parameterR1i xi
inNamed1
Named1
contains an entry (optional or required) of the formR1i xi
for every required named parameterR0i xi
inNamed0
- The result is defined as follows:
R0
is UP(T0
,T1
)B2i
isB0i
P2i
is DOWN(P0i
,P1i
)Named2
contains exactlyR2i xi
for eachxi
in bothNamed0
andNamed1
- where
R0i xi
is inNamed0
- where
R1i xi
is inNamed1
- and
R2i
is DOWN(R0i
,R1i
) - and
R2i xi
is required ifxi
is required in eitherNamed0
orNamed1
- each
-
UP(
T Function<...>(...)
,S Function<...>(...)
) =Function
otherwise -
UP(
T Function<...>(...)
,T2
) = UP(Object
,T2
) -
UP(
T1
,T Function<...>(...)
) = UP(T1
,Object
) -
UP(
(...)
,Record
) =Record
-
UP(
Record
,(...)
) =Record
-
UP(
(S0, ... Sk, {T0 d0, ..., Tn dn})
,(S0', ... Sk', {T0' d0, ..., Tn' dn})
) =(Q0, ...,Qk, {R0, ..., Rn})
if:Qi
is UP(Si
,Si'
)Ri
is UP(Ti
,Ti'
)
-
UP(
(...)
,(...)
) =Record
otherwise -
UP(
(...)
,T2
) = UP(Object
,T2
) -
UP(
T1
,(...)
) = UP(T1
,Object
) -
UP(
FutureOr<T1>
,FutureOr<T2>
) =FutureOr<T3>
whereT3
= UP(T1
,T2
) -
UP(
Future<T1>
,FutureOr<T2>
) =FutureOr<T3>
whereT3
= UP(T1
,T2
) -
UP(
FutureOr<T1>
,Future<T2>
) =FutureOr<T3>
whereT3
= UP(T1
,T2
) -
UP(
T1
,FutureOr<T2>
) =FutureOr<T3>
whereT3
= UP(T1
,T2
) -
UP(
FutureOr<T1>
,T2
) =FutureOr<T3>
whereT3
= UP(T1
,T2
) -
UP(
T1
,T2
) =T2
ifT1
<:T2
- Note that both types must be class types at this point
-
UP(
T1
,T2
) =T1
ifT2
<:T1
- Note that both types must be class types at this point
-
UP(
C<T0, ..., Tn>
,C<S0, ..., Sn>
) =C<R0,..., Rn>
whereRi
is UP(Ti
,Si
) -
UP(
C0<T0, ..., Tn>
,C1<S0, ..., Sk>
) = least upper bound of two interfaces as in Dart 1, with modifications for handling mixed null safe and legacy code as follows:- For an upper bound computation in a legacy library, the set of
super-interfaces used consists of the
LEGACY_ERASURE
of the super-interfaces of the two types. - For an upper bound computation in an opted in library, no modification of the set of super-interfaces is performed.
- For an upper bound computation in a legacy library, the set of
super-interfaces used consists of the
Note that this algorithm is defined for types, not type schemas. For the upper bound of two type schemas, see the inference specification.
We define the lower bound of two types T1 and T2 to be DOWN(T1,T2) as follows.
-
DOWN(
T
,T
) =T
-
DOWN(
T1
,T2
) where TOP(T1
) and TOP(T2
) =T1
if MORETOP(T2
,T1
)T2
otherwise
-
DOWN(
T1
,T2
) =T2
if TOP(T1
) -
DOWN(
T1
,T2
) =T1
if TOP(T2
) -
DOWN(
T1
,T2
) where BOTTOM(T1
) and BOTTOM(T2
) =T1
if MOREBOTTOM(T1
,T2
)T2
otherwise
-
DOWN(
T1
,T2
) =T2
if BOTTOM(T2
) -
DOWN(
T1
,T2
) =T1
if BOTTOM(T1
) -
DOWN(
T1
,T2
) where NULL(T1
) and NULL(T2
) =T1
if MOREBOTTOM(T1
,T2
)T2
otherwise
-
DOWN(
Null
,T2
) =Null
ifNull <: T2
Never
otherwise
-
DOWN(
T1
,Null
) =Null
ifNull <: T1
Never
otherwise
-
DOWN(
T1
,T2
) where OBJECT(T1
) and OBJECT(T2
) =T1
if MORETOP(T2
,T1
)T2
otherwise
-
DOWN(
T1
,T2
) where OBJECT(T1
) =T2
ifT2
is non-nullable- NonNull(
T2
) if NonNull(T2
) is non-nullable Never
otherwise
-
DOWN(
T1
,T2
) where OBJECT(T2
) =T1
ifT1
is non-nullable- NonNull(
T1
) if NonNull(T1
) is non-nullable Never
otherwise
-
DOWN(
T1*
,T2*
) =S*
whereS
is DOWN(T1
,T2
) -
DOWN(
T1*
,T2?
) =S*
whereS
is DOWN(T1
,T2
) -
DOWN(
T1?
,T2*
) =S*
whereS
is DOWN(T1
,T2
) -
DOWN(
T1*
,T2
) =S
whereS
is DOWN(T1
,T2
) -
DOWN(
T1
,T2*
) =S
whereS
is DOWN(T1
,T2
) -
DOWN(
T1?
,T2?
) =S?
whereS
is DOWN(T1
,T2
) -
DOWN(
T1?
,T2
) =S
whereS
is DOWN(T1
,T2
) -
DOWN(
T1
,T2?
) =S
whereS
is DOWN(T1
,T2
) -
DOWN(
T0 Function<X0 extends B00, ... Xm extends B0m>(P00, ... P0k)
,T1 Function<X0 extends B10, ... Xm extends B1m>(P10, ... P1l)
=R0 Function<X0 extends B20, ..., Xm extends B2m>(P20, ..., P2q)
if:- each
B0i
andB1i
are equal types (syntactically) q
is max(k
,l
)R0
is DOWN(T0
,T1
)B2i
isB0i
P2i
is UP(P0i
,P1i
) fori
<= than min(k
,l
)P2i
isP0i
fork
<i
<=q
P2i
isP1i
forl
<i
<=q
P2i
is optional ifP0i
orP1i
is optional, or if min(k, l) < i <= q
- each
-
DOWN(
T0 Function<X0 extends B00, ... Xm extends B0m>(P00, ... P0k, Named0)
,T1 Function<X0 extends B10, ... Xm extends B1m>(P10, ... P1k, Named1)
=R0 Function<X0 extends B20, ..., Xm extends B2m>(P20, ..., P2k, Named2)
if:- each
B0i
andB1i
are equal types (syntactically) R0
is DOWN(T0
,T1
)B2i
isB0i
P2i
is UP(P0i
,P1i
)Named2
containsR2i xi
for eachxi
in bothNamed0
andNamed1
- where
R0i xi
is inNamed0
- where
R1i xi
is inNamed1
- and
R2i
is UP(R0i
,R1i
) - and
R2i xi
is required ifxi
is required in bothNamed0
andNamed1
- where
Named2
containsR0i xi
for eachxi
inNamed0
and notNamed1
- where
xi
is optional inNamed2
- where
Named2
containsR1i xi
for eachxi
inNamed1
and notNamed0
- where
xi
is optional inNamed2
- where
- each
-
DOWN(
T Function<...>(...)
,S Function<...>(...)
) =Never
otherwise -
DOWN(
(S0, ... Sk, {T0 d0, ..., Tn dn})
,(S0', ... Sk', {T0' d0, ..., Tn' dn})
) =(Q0, ...,Qk, {R0, ..., Rn})
if:Qi
is DOWN(Si
,Si'
)Ri
is DOWN(Ti
,Ti'
)
-
DOWN(
(...)
,(...)
) =Never
otherwise -
DOWN(
T1
,T2
) =T1
ifT1
<:T2
-
DOWN(
T1
,T2
) =T2
ifT2
<:T1
-
DOWN(
FutureOr<T1>
,FutureOr<T2>
) =FutureOr<S>
- where
S
is DOWN(T1
,T2
)
- where
-
DOWN(
FutureOr<T1>
,Future<T2>
) =Future<S>
- where
S
is DOWN(T1
,T2
)
- where
-
DOWN(
Future<T1>
,FutureOr<T2>
) =Future<S>
- where
S
is DOWN(T1
,T2
)
- where
-
DOWN(
FutureOr<T1>
,T2
) =S
- where
S
is DOWN(T1
,T2
)
- where
-
DOWN(
T1
,FutureOr<T2>
) =S
- where
S
is DOWN(T1
,T2
)
- where
-
DOWN(
T1
,T2
) =Never
otherwise
The definition of upper bound for type variables does not guarantee termination. Counterexample:
void foo<T extends List<S>, S extends List<T>>() {
T x;
S y;
var a = (x == y) ? x : y;
}
It should be changed to close the bound with respect to all of the type variables declared in the same scope, using the greatest closure definition.
The CFE currently implements upper bounds for generic functions incorrectly. Example:
typedef G0 = T Function<T>(T x);
typedef G1 = T Function<T>(T x);
void main() {
G0 x;
G1 y;
// Analyzer: T Function<T>(T)
// CFE: bottom -> Object
var a = (x == y) ? x : y;
}
Both the CFE and the analyzer currently implement lower bounds for generic functions incorrectly. Example:
typedef G0 = T Function<T>(T x);
typedef G1 = T Function<T>(T x);
void main() {
void Function(G0) x;
void Function(G1) y;
int z;
// Analyzer: void Function(Never Function(Object))
// CFE: void Function(bottom-type Function(Object))
var a = (x == y) ? x : y;
}
The current algorithm is asymmetric. There is an equivalence class of top types, and we correctly choose a canonical representative for bare top types using the MORETOP predicate. However, when two different top types are embedded in two mutual subtypes, we don't correctly choose a canonical representative.
import 'dart:async';
void main () {
List<FutureOr<Object>> x;
List<dynamic> y;
String s;
// List<dynamic>
var a = (x == y) ? x : y;
// List<FutureOr<Object>>
var b = (x == y) ? y : x;
The best solution for this is probably to normalize the types. This is fairly
straightforward: we just normalize FutureOr<T>
to the normal form of T
when
T
is a top type. We can then inductively apply this across the rest of the
types. Then, whenever we have mutual subtypes, we just return the normal form.
This would be breaking, albeit hopefully only in a minor way.
An alternative would be to try to define an ordering on mutual subtypes. This
can probably be done, but is a bit ugly. For example, consider Map<dynamic, FutureOr<dynamic>>
vs Map<FutureOr<dynamic>, dynamic>
. The obvious way to
proceed is to define the total order by defining a traversal order on types, and
then defining the ordering lexicographically. That is, saying that T
is
greater than S
if the first pair of top types encountered in the traversal
that are not identical are T0
and S0
respectively, and MORETOP(T0
,
S0
).
A similar treatment would need to be done for the bottom types as well, since there are two equivalences there.
X extends T
is equivalent toNull
ifT
is equivalent toNull
.FutureOr<Null>
is equivalentFuture<Null>
.
A possible variant of the previous approach would be to define a finer grained
variant of the subtyping relation which is a total order on mutual subtypes.
That is, if <::
is the extended relation, we would want that T <:: S
implies
that T <: S
, but also that T <:: S
and S <:: T
implies that S
and T
are syntactically (rather than just semantically) equal.