dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.27k stars 1.58k forks source link

Discrepancy during type inference of F-bounded type #50025

Open eernstg opened 2 years ago

eernstg commented 2 years ago

Consider the following program:

abstract class C<X extends C<X>> {}
class D1 extends C<D1> {}
X f<X extends C<X>>(X x1, X x2) => x1;

void main() => print(f(D1(), D1()));

This program is accepted by the analyzer (from ) with no issues, but the CFE reports an inference failure:

n008.dart:5:22: Error: Inferred type argument 'C<Object?>' doesn't conform to the bound 'C<X>' of the type variable 'X' on 'f'.
 - 'C' is from 'n008.dart'.
 - 'Object' is from 'dart:core'.
Try specifying type arguments explicitly so that they conform to the bounds.
void main() => print(f(D1(), D1()));
                     ^
n008.dart:3:5: Context: This is the type variable whose bound isn't conformed to.
X f<X extends C<X>>(X x1, X x2) => x1;
    ^

The inference failure disappears if main calls f directly (that is, the returned result isn't printed).

Is this a bug in CFE type inference? In any case, it would be expected that the CFE and the analyzer agree on the outcome, so I've used the label 'type-bug'.

eernstg commented 2 years ago

The discrepancy actually still exists for this example, which is a bit simpler:

abstract class C<X extends C<X>> {}
class D1 extends C<D1> {}
X f<X extends C<X>>(X x) => x;

void main() => f(D1());
chloestefantsova commented 2 years ago

The issue seems to be in the return type void of main. It's treated as a not-unknown upper constraint on the type variable being inferred, which results in the top-down inference deciding the type. In this case that's the conjunction of the constraints by void and C<Object?>.

If void is replaced by dynamic, which the CFE treats as UnknownType when it solely plays the type context for the expression being inferred, the top-down part of the inference leaves the type undecided, and the bottom-up part yields D1, as it happens in the case of the Analyzer.

The case of Object? is similar to that of void: it forces the CFE to use the top-down inferences to provide the type, and the Analyzer yields no error.

My current hypothesis is that the Analyzer treats all top types as empty context in inference. I'll try to see if we can do the same in the CFE and if it breaks anything.

eernstg commented 2 years ago

That sounds like a tricky decision (like everything in type inference ;-). There is one situation where I'd think it creates a problem if every top type is considered to be the empty context during inference, and that is during computation of the return type of a function literal.

void main() {
  void Function() f = () {
    return 3; // Error.
  };
}

In this example, we should get the error, because the return 3; statement is treated like the same return statement would be treated in a regular (named) function or method declaration. If we consider the return type of the context type of the function literal to be the empty context then the function literal will just get the return type int. The function literal will then have type int Function(), and that's perfectly assignable to void Function(), so the desired error message is gone.

It would probably be useful to get some input from @scheglov, in order to make sure the CFE and the analyzer handle the situation in ways that don't differ in any essential manner.

scheglov commented 2 years ago

Not sure what is the question. The analyzer does report a compile-time error for this example with a local function. We get the context type void Function() from the variable declaration, and set the return type void as the imposed type for the body.

IIRC, for =>, i.e. expression function body, we have an exception, that it is OK to return a value into void.

For

abstract class C<X extends C<X>> {}
class D1 extends C<D1> {}
X f<X extends C<X>>(X x) => x;

void main() => f(D1());

the analyzer infers D1 for X, using constraints:

0 = {map entry} [TypeParameterElementImpl] -> [_GrowableList]
 key = {TypeParameterElementImpl} X extends C<X>
 value = {_GrowableList} size = 3
  0 = {_TypeConstraint} 'X extends C<X>' must extend 'void'
  1 = {_TypeConstraint} 'D1' must extend 'X extends C<X>'
  2 = {_TypeConstraint} 'X extends C<X>' must extend 'C<D1>'
eernstg commented 2 years ago

Not sure what is the question

I just noticed that the CFE and the analyzer disagree, so the overall question would be "What's the correct behavior for the following example?"

abstract class C<X extends C<X>> {}
class D1 extends C<D1> {}
X f<X extends C<X>>(X x) => x;

void main() => f(D1());

But looking at the constraints produced by the analyzer, I'm wondering whether the CFE omits the creation of this constraint:

  2 = {_TypeConstraint} 'X extends C<X>' must extend 'C<D1>'

@stereotype441, @chloestefantsova, @scheglov, it looks like the approaches taken by the analyzer and the CFE aren't quite identical. Is there a place where the behavior of this part of type inference is specified?

stereotype441 commented 2 years ago

I don't know of a place where this particular rule is specified. Generally type inference is not very well specified, and even when it is, a lot of the specs for it were written without consulting the analyzer and CFE implementations, so I generally don't consider the specs to be authoritative. That said, I believe @leafpetersen has been working on writing up some specs for type inference, so he may know where to look for a spec of this particular behavior.