sdk
sdk copied to clipboard
LUB missing case
Repro:
// Items
sealed class Foo {
const Foo();
}
final class Bar extends Foo {
const Bar();
}
final class Baz extends Foo {
const Baz();
}
// Holders
sealed class Alpha<T extends Foo> {
const Alpha({required this.first});
final T first;
}
sealed class Beta<T extends Foo, U extends Foo> extends Alpha<T> {
Beta({required this.second, required super.first});
final U second;
}
abstract interface class AlphaBar extends Alpha<Bar> {
AlphaBar({required super.first});
}
abstract interface class AlphaBaz extends Alpha<Baz> {
AlphaBaz({required super.first});
}
// Concrete holders for items
final class BetaBarFoo<T extends Foo> extends Beta<Bar, T>
implements AlphaBar {
BetaBarFoo({required super.second, required super.first});
}
final class BetaBazFoo<T extends Foo> extends Beta<Baz, T>
implements AlphaBaz {
BetaBazFoo({required super.second, required super.first});
}
void foo(BetaBazFoo q, BetaBarFoo p) {
final list = [q, p];
}
I was expecting list to be at least List<Alpha<Foo>>, but I get List<Object>. It does work fine if I add Alpha/Beta as the literal generic.
I'm not sure if my expectations are correct.
@dart-lang/language-team
I was testing this on stable. Just copied this on master, and it seems to be working. Not sure if this was intentionally fixed, maybe add some tests?
That looks like it's working as intended. Not optimally, but what is actually specified. The two types in the list, and their superinterfaces, are
BetaBazFoo<Foo><:Beta<Baz, Foo><:Alpha<Baz><:ObjectBetaBarFoo<Foo><:Beta<Bar, Foo><:Alpha<Bar><:Object
The only common supertype is Object.
There are cases where the Up algorithm tries to unify inside type arguments, but that's only when the outer type is the same, so List<int> and List<double> can be unified to List<num>, but List<int> and Iterable<num> will only try to compare actual superinterfaces, and the only common superinterface of those two is Object.
I see, but both Alpha<Bar> and Alpha<Baz> are Alpha<Foo> and then Object, and it seems to be working on master, as I mentioned in my last comment.
I'd be ecstatic if we've made a better UP, but I don't remember any recent changes that should make this work.
(Doesn't mean there wasn't one, just that I don't remember.
@stereotype441, I don't think inference-using-bounds should affect this, and not sound-type-inference either. The only other experiment I can see is it type-inference-4, which doesn't look like it's enabled on the main branch.)
I opened the example again (maybe slow analyzer? I'm on Windows and it would not be the first time), it seems I was mistaken, it doesn't know to use Alpha<Foo> (I could swear I saw it before, but I'm not sure now).
Anyway, if that is not working, I'll leave this as a request for enhancement.
I feel like if it knows that List<int> and List<double> can be unified to List<num>, it should probably use the same reasoning to unify Alpha<Bar> and Alpha<Baz> to Alpha<Foo>.
This is a more concise example:
sealed class Foo {
const Foo();
}
final class Bar extends Foo {
const Bar();
}
final class Baz extends Foo {
const Baz();
}
sealed class Alpha<T extends Foo> {
const Alpha({required this.first});
final T first;
}
abstract interface class AlphaBar extends Alpha<Bar> {
AlphaBar({required super.first});
}
abstract interface class AlphaBaz extends Alpha<Baz> {
AlphaBaz({required super.first});
}
void bar(AlphaBar q, AlphaBaz p) {
final v = 1 == 1 ? q : p;
final list = [q, p];
}
void baz(Alpha<Bar> q, Alpha<Baz> p) {
final v = 1 == 1 ? q : p;
final list = [q, p];
}
While bar makes v and list use Object, you can see that baz makes the right decision and uses Alpha<Foo>.
Here is an even more concise example:
void foo(bool b, Iterable<int> iter, List<num> list) {
var x = b ? iter : list;
x.expectStaticType<Exactly<Object>>; // This implies that `x` has type `Object`.
}
typedef Exactly<X> = X Function(X);
extension<X> on X {
X expectStaticType<Y extends Exactly<X>>() => this;
}
Here's a proposal which would handle many of these cases: https://github.com/dart-lang/language/issues/3282. See also the overall topic https://github.com/dart-lang/language/issues?q=state%3Aopen%20label%3A%22least-upper-bound%22.
Another case from the same project. I have a specific set of enums that all add the same mixin:
abstract mixin class Base {
// Some members here
}
enum E1 with Base { v }
enum E2 with Base { v }
enum E3 with Base { v }
void f() {
var myList = [E1.v, E2.v, E3.v];
}
For the above, myList is neither Enum or Base, but Object yet again. For this specific case I could see a point of having them not show any base type because the user may want either, but it still feels weird. Changing between with and implements makes no difference here too.
I'd say:
- When a list of
Enums that have no other shared super type in common, choseEnum(this would allow access toindexandname. - If all enums do share another common subtype, defer to it instead.
I will note that if you make Base a mixin-only and add on Enum (or implement Enum for the mixin class) it will calculate the correct type of Base for the list.
Edit: I found a problem with this setup, see: https://github.com/dart-lang/sdk/issues/60965.