-
Notifications
You must be signed in to change notification settings - Fork 205
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
Function literal return type inference fails on divergent future #3148
Comments
Let's work towards #870, and get rid of the implicit Looking at: class Divergent<T> implements Future<Divergent<Divergent<T>>> {
noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() => () async => Divergent<int>();
Not sure I follow that, or agree. There is a number of types involved here. I'll use my own names, since I don't remember official names.
Here the context provides nothing nothing, and we start at the return expression static type of The expression type must be assignable to So the returned value type is This is the only return we have, so the only constraint on the future value type, so the future value type should also be found to be Then the function return type must be a supertype of That seems like it should work. The extra flatten introduced in Basically, to infer the return type of an async function with no context type scheme, Then the runtime semantics are that return will do, effectively: var value = e;
FutureOr<FVT> futureOrValue = value;
if (futureOrValue is Future<FVT>) real_return await futureOrValue;
real_return futureOrValue; That should be safe, since the FVT is a supertype of flatten of the static type of (each) |
@eernstg The spec you link to (which I think I wrote) looks buggy to me. In particular, it applies flatten twice, which seems wrong to me, is there a reason it does so that I'm missing? That is, we have:
which in the case of an async function does a flatten to produce an actual return type T, and then:
where I don't see why doing two flattens here is correct. Am I missing something, or is this a bug? Presumably in most cases doing a double flatten doesn't do anything, but in some cases it does (and seems wrong). Simpler example: void test1() async {
var f = Future<Future<int>>.value(Future<int>.value(3));
var g = () async => (f as dynamic);
print(g());
print(await g());
}
void test2() async {
var f = Future<Future<int>>.value(Future<int>.value(3));
// var g = () async => f;
// print(await g());
}
void main() {
test1();
test2();
} Running this program prints out:
Implying ( as expected ) that semantically we are doing a single await on the return value, returning a value of type If you uncomment the second line in the second test, you get the static error:
which is as specified, but seems wrong. |
Good catch! I didn't have the intuition firmly enough in place to tell me that the second @lrhn wrote:
@leafpetersen wrote:
Right, it is also my understanding that the last use of
The updates are 'let |
Cf. #3149. |
We have more problems, real soundness problems, with the generator functions. import"dart:async";
void main() {
FutureOr<Iterable<int>?> o1 = function();
// Already here we have an `Iterable<dynamic>` with a static type which implies `Iterable<int>`.
o1..l; // (1, null): _SyncStarIterable<dynamic> @ FutureOr<Iterable<int>?>
// Hard to prove, since dart2js assumes soundness and doesn't do type checks which "can't fail".
FutureOr<Iterable<int>?> o2 = function() as dynamic; // Error optimized away.
o2 as FutureOr<Iterable<int>?>; // Error optimizied away.
// Promote the other union types away.
if (o2 is Future<Iterable<int>?>) {
print("not");
} else if (o2 == null) {
print("not");
} else {
// Now static type is `Iterable<int>`, runtime type is `Iterable<dynamic>`. Unsoundness achieved!
o2..l; // (1, null): _SyncStarIterable<dynamic> @ Iterable<int>
}
var t = DateTime.now().millisecondsSinceEpoch > 0; // Unpredictable true
(t ? o2 : "") as FutureOr<Iterable<int>?>; // Finally throws!
}
// Return type of supertype of `Iterable<Never>`, so allowed.
// Inferred value type is `dynamic`, because we don't find the `int`. That's unsound.
FutureOr<Iterable<int>?> function() sync* {
yield 1;
yield null; // Should not be allowed. Is.
}
extension <T> on T {
void get l {
print("$this: $runtimeType @ $T");
}
} (Same issue for This is from dart2js, so I assume all compilers are affected to some extent. They may fail earlier if they don't optimize away as many "safe" type-checks. The VM fails on the first downcast from On the other hand, with no declaration, but only a context type, it works fine: FutureOr<Stream<int>?> Function() f = () async* {
yield 1;
yield null;
}; gives an error because Probably because the inferred "element type" from the context is incorrectly set to |
The language specification does indeed specify a generator element type for the return type |
Is it ever useful to have a return type which is not actually a subtype of |
It's no more or less reasonable to return But it can actually be reasonable to have a return type of There is no good reason to disallow supertypes of And it's not like we can't figure it out in other places: FutureOr<Iterable<double>?> Function() l1 =
()=> [42];
FutureOr<Iterable<double>?> Function() l2 =
() sync* { yield* [42]; }; The first declaration "just works". |
@leafpetersen wrote:
As always, the return type of any given method could be forced by overrides: class A {
FutureOr<Iterable<int>?> m() => null;
}
class B extends A {
FutureOr<Iterable<int>?> m() sync* { yield 1; yield 2; }
}
class C extends B {
FutureOr<Iterable<int>?> m() => Future.value();
} One might argue that an error on |
The quickest fix here would be to insert the check that the generated function type is actually assignable to the context type. Since it's not guaranteed. Then we can fix the spec to allow more things to insert compilable types. |
Ok, that makes sense. Seems a bit stretched, but in principle it's reasonable. |
Consider the following program:
This program is rejected by the CFE and the analyzer with similar error messages. Analyzer:
CFE:
The return type inference for a function literal is specified here.
Checking the current definition of
flatten
we getflatten(Divergent<T>) == Divergent<Divergent<T>>
.For the inference of the return type of the function literal, we note that the imposed return type schema has no relevant information (and we get the same error with
var _ = /*same function literal*/;
), and also that the returned expression has typeDivergent<int>
as stated, which is then also the type of the returned expression after type inference for any context type because there's no thing for type inference to do.So the actual returned type is
flatten(Divergent<int>)
, that is,Divergent<Divergent<int>>
. This implies that the inferred return typeR
for the function literal isFuture<flatten(Divergent<Divergent<int>>)>
, that is,R == Future<Divergent<Divergent<Divergent<int>>>>
.This shows that the implementations are computing the specified return type, and the only remaining problem is that the given returned expression does not satisfy the typing constraints associated with that return type.
So is it justified that we're returning a
Divergent<int>
in anasync
function whose return type isR == Future<Divergent<Divergent<Divergent<int>>>>
? Should we get an error or not?Let's explore some parts of the supertype graph of
Divergent<int>
, in order to see whether or not we can find a supertype which should be accepted. If this is true thenDivergent<int>
should arguably also be accepted.By the declaration,
Divergent<int> <: Future<Divergent<Divergent<int>>>
. Other supertypes can be created by a rewrite operation that changesDivergent<T>
toFuture<Divergent<Divergent<T>>>
for anyT
. No matter which steps we take, this will yield a type of the formFuture^k<(Divergent|Future)^m<int>>
for somek >0
. LetS
be the type we computed by any set of steps like this.The test is that
Future<flatten(S)>
must be assignable to the return typeR
, that is, it must be a subtype ofR
. However, that is never true because the expansion steps described above will always create anS
whereFuture
is applied to the type argument at some level of nesting for any number of expansion steps greater than 1, and the typesDivergent<int>
andFuture<Divergent<Divergent<int>>>
(zero steps plus one step) are not subtypes ofR
.The rules about inference of the return type of a function literal are surely intended to satisfy the sanity condition that each returned expression must satisfy the typing requirements for a returned expression, but that doesn't hold in this particular case.
In general, we don't want type inference (of any kind) to yield a program with type errors based on the failure to satisfy a subtype requirement where an inferred type is one of the operands. ("If we infer a type then it must be a type that works.")
However, the conclusion is probably the following:
Future<Object?>
or something like that.Note that, presumably, the class
Divergent
has a very unusual typing structure, and it is not obvious that anything with that structure would be useful or important to support.@dart-lang/language-team, WDYT? Do we just keep everything as it is? Or do we use the return type
Future<Object?>
(or something like that) when this kind of type error occurs?The text was updated successfully, but these errors were encountered: