-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bounds on generic methods should be treated invariantly in subtyping #29014
Comments
Clarification, does this affect function subtyping in general?
It seems like this is already an error, so I'm assuming this is truly limited to method overrides. |
this is also currently not an error:
Would the error be at override, or would it infer the type parameter to have a bound, in which case the error would be at the type application site of @leafpetersen for these questions |
Oops, had tested contravariance before. Function type's type parameters are currently covariant inside the type system, should that change too?
|
@MichaelRFairhurst I'm confused about the results you claim to see above. Just tested with bleeding edge analyzer and I see covariance as an error, contra-variance currently ok: void main() {
Function<T extends num>() fn = <T extends int>(){}; // Error
Function<T extends int>() fn2 = <T extends num>(){}; // Currently OK, should be error.
} The contra-variant case should also be an error. This applies to all places that subtype checking is done (override, assignment, etc). This example: class C extends A {
void foo<T>() {
return foo<String>();
}
} I don't understand. There is no subtype checking involved here? In any case, this is valid code: Dart has polymorphic recursion. |
I was confused at the results I claimed to see! And I figured out why. I had a copy/paste error. The code I said 'is already an error" and the code I implied wasn't an error, are the same code. Then I based my question on the wrong code snippet. Anyways, I am seeing the same behavior as you. And also, that answers my question. Easy to solve for all cases. The question on the last example refers to class |
I had a prototype in an hour or so, but this may be another one of those rabbit hole bugs. there was an issue in function type equality not present in function type subtype code. The == operator checks that the typeArguments of two functions are equal, where the subtype code just checks that their typeFormals match as well as their parameters & return type. I could be mistaken, but it seems that the typeArguments should not be checked since those include values bound to outer contexts. (unless the goal is to say, "these are the same type in the same context". That said, == is used in a number of places in TypeSystem that makes me think that is NOT the case). The problem is that having equality be too strict means that switching from co to invariance creates bugs, unless I can fix that equality. Its easy to fix, but I'm getting at least one very strange failure where code seems to depend upon the "broken" (if it is) equality behavior. Its in code that I've worked on before, where a robust solution was not easy and I had to sort of create some compromises. I don't think any of these failures will be permissible to leave in. So I'm puzzling over what an "incremental" rollout would be, and in the meantime will try to solve that test failure without creating others. Its worth noting that the test failure is in summary linking, so my understanding is that language tests don't really provide coverage there. If I can't solve that test case, I'll mark it |
Right, no override inference for bounds, so this is rejected. |
Function type equality is way sketchy in the analyzer and probably needs some revisiting.
I think you're referring to the analyzer's lazy substitutions? You definitely need to distinguish between the same syntactic type with different replacements for free variables. For example, this program needs to be rejected: class A<T> {
void foo<S extends T>() {}
}
class B<T> {
void foo<S extends T>() {}
}
void main() {
var f = A<int>().foo;
var g = B<String>().foo;
f = g;
} |
maybe @jmesserly can give some insight into the equality stuff. In the following example,
if I check that the bounds are equal, I get "false." The reason why is that A.foo.R.bound.typeArguments has a length of 2: [R, T], and B.foo.R2.bound.typeArguments has a length of 1: [R2]. Neither of these values are related to the equality of the two Function types though. It already checks that I suppose I may need to alter the code of |
I think the fix is something like this. Warning, untested code :) Note that I'm not using diff --git a/pkg/analyzer/lib/src/dart/element/type.dart b/pkg/analyzer/lib/src/dart/element/type.dart
index 5d91528495..f752be1ad7 100644
--- a/pkg/analyzer/lib/src/dart/element/type.dart
+++ b/pkg/analyzer/lib/src/dart/element/type.dart
@@ -1175,8 +1175,8 @@ class FunctionTypeImpl extends TypeImpl implements FunctionType {
/**
* Compares two function types [t] and [s] to see if their corresponding
- * parameter types match [parameterRelation] and their return types match
- * [returnRelation].
+ * parameter types match [parameterRelation], return types match
+ * [returnRelation], and type parameter bounds match [boundsRelation].
*
* Used for the various relations on function types which have the same
* structural rules for handling optional parameters and arity, but use their
@@ -1184,14 +1184,20 @@ class FunctionTypeImpl extends TypeImpl implements FunctionType {
*
* If [parameterRelation] is omitted, uses [returnRelation] for both. This
* is convenient for Dart 1 type system methods.
+ *
+ * If [boundsRelation] is omitted, uses [returnRelation]. This is for
+ * backwards compatibility, and convenience for Dart 1 type system methods.
*/
static bool relate(
FunctionType t,
DartType other,
bool returnRelation(DartType t, DartType s),
DartType instantiateToBounds(DartType t),
- {bool parameterRelation(ParameterElement t, ParameterElement s)}) {
+ {bool parameterRelation(ParameterElement t, ParameterElement s),
+ bool boundsRelation(DartType bound2, DartType bound1,
+ TypeParameterElement formal2, TypeParameterElement formal1)}) {
parameterRelation ??= (t, s) => returnRelation(t.type, s.type);
+ boundsRelation ??= (t, s, _, __) => returnRelation(t, s);
// Trivial base cases.
if (other == null) {
@@ -1209,7 +1215,7 @@ class FunctionTypeImpl extends TypeImpl implements FunctionType {
FunctionType s = other as FunctionType;
if (t.typeFormals.isNotEmpty) {
List<DartType> freshVariables =
- relateTypeFormals(t, s, (s, t, _, __) => returnRelation(s, t));
+ relateTypeFormals(t, s, boundsRelation);
if (freshVariables == null) {
return false;
}
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index 185d6d3489..84e7e4627c 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -6748,6 +6748,9 @@ class ResolverVisitor extends ScopedVisitor {
*/
void _inferFunctionExpressionParametersTypes(
Expression mayBeClosure, DartType mayByFunctionType) {
+ // TODO(jmesserly): remove this code and callers. It's doing
+ // "propagated type" inference for the Dart 1 type system.
+ assert(!strongMode);
// prepare closure
if (mayBeClosure is! FunctionExpression) {
return;
diff --git a/pkg/analyzer/lib/src/generated/type_system.dart b/pkg/analyzer/lib/src/generated/type_system.dart
index d178f25039..ab376a17a8 100644
--- a/pkg/analyzer/lib/src/generated/type_system.dart
+++ b/pkg/analyzer/lib/src/generated/type_system.dart
@@ -646,18 +646,14 @@ class GenericInferrer {
if (t1 is FunctionType && t2 is FunctionType) {
return FunctionTypeImpl.relate(
- t1,
- t2,
- (t1, t2) {
- // TODO(jmesserly): should we flip covariance when we're relating
- // type formal bounds? They're more like parameters.
- return matchSubtype(t1, t2);
- },
- _typeSystem.instantiateToBounds,
+ t1, t2, matchSubtype, _typeSystem.instantiateToBounds,
parameterRelation: (p1, p2) {
return _matchSubtypeOf(p2.type, p1.type, null, origin,
covariant: !covariant);
- });
+ },
+ // Type parameter bounds are invariant.
+ boundsRelation: (t1, t2, p1, p2) =>
+ matchSubtype(t1, t2) && matchSubtype(t2, t1));
}
if (t1 is FunctionType && t2 == typeProvider.functionType) {
@@ -1181,7 +1177,10 @@ class StrongTypeSystemImpl extends TypeSystem {
/// - it allows opt-in covariant parameters.
bool isOverrideSubtypeOf(FunctionType f1, FunctionType f2) {
return FunctionTypeImpl.relate(f1, f2, isSubtypeOf, instantiateToBounds,
- parameterRelation: isOverrideSubtypeOfParameter);
+ parameterRelation: isOverrideSubtypeOfParameter,
+ // Type parameter bounds are invariant.
+ boundsRelation: (t1, t2, p1, p2) =>
+ isSubtypeOf(t1, t2) && isSubtypeOf(t2, t1));
}
/// Check that parameter [p2] is a subtype of [p1], given that we are
@@ -1522,7 +1521,10 @@ class StrongTypeSystemImpl extends TypeSystem {
/// Check that [f1] is a subtype of [f2].
bool _isFunctionSubtypeOf(FunctionType f1, FunctionType f2) {
return FunctionTypeImpl.relate(f1, f2, isSubtypeOf, instantiateToBounds,
- parameterRelation: (p1, p2) => isSubtypeOf(p2.type, p1.type));
+ parameterRelation: (p1, p2) => isSubtypeOf(p2.type, p1.type),
+ // Type parameter bounds are invariant.
+ boundsRelation: (t1, t2, p1, p2) =>
+ isSubtypeOf(t1, t2) && isSubtypeOf(t2, t1));
}
bool _isInterfaceSubtypeOf( |
I made that diff by searching for all uses of I also looked at class C<T> {
g<S extends T>() => <S>[];
}
class D extends C<num> {
g<R extends num>() => <R>[];
}
main() {
C<Object> c = new C<int>();
c.g<String>(); // must throw for soundness
c = new D();
c.g<String>(); // must throw for soundness
} |
Ah, I like "isSubtypeOf(t1, t2) && isSubtypeOf(t2, t1)"! And great edge case with object/dynamic. With this I can leave equality untouched which hopefully fixes that linking error if it depends upon equality being...what it is! Awesome response & response time @jmesserly! |
Your code is awesome @jmesserly and passed all my tests locally. Up for review here, running trybots: https://dart-review.googlesource.com/c/sdk/+/56029 |
Currently, the analyzer treats generic method bounds contra-variantly for sub-typing purposes. For example, the following is allowed:
Per language team decision, this should be changed to require that bounds be equal.
The text was updated successfully, but these errors were encountered: