-
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
How should intersection typed receivers be handled during extension method resolution? #56028
Comments
An extension applies to a type iff the static type of the extension is a subtype of the extension Ext<X> on Foo<X> {
...
} applies to a static type T iff class ExtClass<X> {
ExtClass(Foo<X> _);
} and it infers type arguments in the same way (but without any context types being involved, the argument was already inferred, and the constructor invocation itself behaves like it's in receiver position). So the current correct behavior is defined by how we do inference on a similar constructor. The analyzer is not doing the correct thing. The error message I get for: class ExtClass<T extends num> {
final T foo;
ExtClass(T this.foo) {
print("ExtClass<$T>");
}
}
void baz<T extends num?>(T value) {
if (value != null) {
// Is intersection type, both `T` and `num`.
T v1 = value;
num v2 = value;
print(v1 == v2);
var e = ExtClass(value).foo;
}
} is, in dart2js:
and in the analyzer:
In both cases, the inferred type argument is T, it's not a valid type argument for a concrete instantiation of The extension method should then do the equivalent type inference, which means inferring extension Ext<T extends num> on T {
T get foo {
print("$this:$runtimeType @ $T .foo");
return this;
}
}
void bar<T extends num?>(T value) {
if (value != null) {
var e = value.foo..st<Exactly<T>>();
}
}
// with the usual:
typedef Exactly<T> = T Function(T);
extension <T> on T {
T st<X extends Exactly<T>>() {
print("$this:$runtimeType @ $T");
return this;
}
} Dart2Js gives the error
which is correct, because the inferred type argument being invalid means that the extension is not applicable. The error here is that the analyzer considers The more interesting question is whether we'd want to change how we choose the projection of an intersection type into a valid type argument. If an intersection type is inferred as a type argument, and the intersection type satisfies (is a subtype of) the type parameter bound, then at least one of the types of the intersection is a subtype of the bound. We currently always choose the type variable as the type argument. If we're already checking whether the intersection type is a subtype of the bound, then there shouldn't be much extra work in remembering which of the intersected types was that (first) proof of being a subtype. But that depends on where in the type inference algorithm we remove the intersection. It seems the analyzer has some issues with intersections and type parameters in general. Take: void foo<V extends num>(V Function(V) f, V v) { f(v); }
void bar<T extends num?>(T v) {
if (v != null) { // v : T & num
foo((x) => x, v);
}
} where dart2js gives an error because
The function type of |
That's a very nice analysis, @lrhn! However, I don't think we can claim that one or the other erasure is as specified, because inference.md is as far as we've gotten with a specification of inference, and it does not mention erasure of intersection types. One thing we did specify is that the future type of Otherwise, I agree that implementations generally erase We've obviously got an issue if any tool considers a function literal (or indeed any expression) to have the type |
We may not be able to say which erasure is required, but we can require the behavior to be consistent. |
tl;dr Based on "normal" invocations, we should erase the intersection type to the improved bound, not to the type variable rd;lt We have already resolved a very similar question in the current language specification when it comes to class/mixin instance members (not extension members):
In other words, a receiver whose static type is When it comes to extension member invocations we have the following (specified here). This seems to be at least somewhat inconsistent relative to the treatment of class/mixin instance members:
E is the given extension declaration, e is the receiver (that might be about to invoke an extension member), and S is the static type of e. This means that until we have a complete specification of type inference we cannot completely determine whether or not the application should succeed (see inference.md), but we can create a program that should behave the same in this respect: class Filterable {
void filterableMember() {}
}
class Filter<T extends Filterable?> {
void test(T t) {
if (t is! Filterable) return; // Promote `t` to `T & Filterable`.
t.filterableMember(); // OK, so promotion did happen.
When(t).when(); // Error from both the CFE and the analyzer.
}
}
class When<T extends Filterable> {
final T it;
When(this.it);
T? when() => null;
} In this case type inference yields However, we would get a behavior which is more consistent with the treatment of class/mixin instance members if we used an erasure to So I'd recommend that we erase an intersection type |
I agree that the interface argument suggests that the extension members should be based on the promoted type. The difference is that while a type variable itself has no members, it gets every member except "The Object 5" from its bound or promotion, where the promotion is known to be a subtype of the bound, extension methods can actually add members to any type.
This is a pathological and tortured example, it may very well be that using the promoted bound is always what you want. The most useful approach is the selective one: the extension type applies if either part of the intersection type would satisfy the type constraints, then we erase to the part that actually did. The reason for all this complexity is that we've taken away your default override strategy. Doing |
Best of both worlds? We could reify an intersection type as the run-time type of the value of the promoted variable, as described in dart-lang/language#3831. |
I'm not entirely sure if this is a new issue (or is intentional) because it still has intersection types being handled, just not during extension method resolution. If it is, please tell me so I can open a new one. Consider: mixin M {
void m();
}
class M1 with M {
const M1();
@override
void m() {}
}
class A<T> {
const A(this.o);
final T o;
}
class B<T extends M> extends A<T> with M {
const B(super.o);
@override
void m() => o.m();
}
void foo<T>(T o) {
late A<T> a;
if (o is M) {
a = B<T>(o); // 'T' doesn't conform to the bound 'M' of the type parameter 'T'. Try using a type that is or is a subclass of 'M'.
} else {
a = A<T>(o);
}
} EditAlso, if you remove And if you add This case of course has a possible solution, that is adding |
It is working as intended when we get a compile-time error at That's true in general: Promotion says something about the type of a specific object (namely the one which is being tested), and a variable is promotable if we can see at compile time that it will refer to that same object for a while, and then we can give that variable the promoted type. But the type variable has its own value and we can't learn anything about that by testing the type of the object. |
I see, thanks for explaining! |
I'd just don't see why in the case where I don't assign any type to I think that's what I'm actually asking here. If your last proposal (#3831) got through, would that solve this as well? Edit: because I could simply not (or I could not see how to) assign a value to |
The actual type argument So we're working on If you're convinced that the surrounding code will behave well (that is, it will satisfy some constraints that aren't expressible in the type system) then you could achieve the same semantics in a way that is guided by some run-time type checks: ...
void foo<T>(T o) {
B<S> fooHelper<S extends M>(S o) {
// In this body the type system knows that `S extends M`,
// so we can create a `B<S>`.
return B<S>(o);
}
late A<T> a;
if (o is M && <T>[] is List<M>) {
// At this time we know that `T extends M`. The type system
// won't recognize that, but we can force it using a dynamic
// invocation of a generic function.
a = (fooHelper as Function)<T>(o);
} else {
a = A<T>(o);
}
}
... The crucial part is that we need to know that PS: dart-lang/language#3831 was a crazy idea, I dropped it again. ;-) |
Would it be reasonable if I created an issue to add some type of check to type parameters? Or is it something that the CFE is against in some way? A way for us to test that so that inside that tested context we could consider |
I think that would be interesting! We've never really explored the idea that type parameters could be subject to type tests or type casts in a way which is similar to the instance operations |
To recapitulate: This is an old issue, and we haven't resolved it. I'd recommend that the interface of the promoted type |
@dart-lang/language-team, can we move on this? |
I skimmed the issue, I don't see what it is we are proposing to move on. My understanding is that the implementations behave consistently, and consistently with the model that we've proposed for thinking about this (treat it as constructor inference). Am I missing something? Is there an inconsistency we need to resolve? Otherwise I think we can close this, I'll go ahead and do. If there's a proposal to make a change, maybe we can pursue it in a clean issue? |
As I mentioned in the initial message of this issue, we get this response:
Ah, of course! It is then an analyzer bug, that is, the analyzer should report an error in the original example. In particular, we do get the error from both tools with this variant: class Filterable {}
class Filter<T extends Filterable?> {
void test(T t) {
if (t is! Filterable) return; // Should promote `t` to `T & Filterable`.
When(t).when(); // Rejected by the analyzer and the CFE.
}
}
class When<T extends Filterable> {
final T t;
When(this.t);
T? when() => null;
}
void main() {} I'll reopen the issue, move it to the SDK, and label it accordingly. |
Labels: |
[June 2024: This issue has been clarified by revisiting the specification. The analyzer should report a compile-time error like the one that the CFE currently reports.]
Cf. #52077 for a situation where this question came up. Cf. #52077 (comment) where @lrhn first clarified the situation.
Consider the following program:
The type of
t
at the method invocationt.when()
isT & Filterable
. In order to invoke the extension methodwhen
, the extension needs to receive a type argument, which implies thatT & Filterable
must be erased to a type which is representable at run time.If it is erased to
T
(which is typical for intersection types when reified), the extension does not match, and we should get a compile time error. This appears to be the approach taken in the CFE.If it is erased to
Filterable
(possibly after tryingT
and failing), the invocation can resolve toWhen.when
, and there are no errors. This may be the approach taken by the analyzer—at least, it does not report any errors for this program.Pragmatically, it seems desirable to try each of the two operands of the intersection type. However, it might create difficulties (say, in terms of an exponential time/space usage) because it is a deviation from the one-pass strategies that we are otherwise using.
So what is the desired approach? @dart-lang/language-team, WDYT?
The text was updated successfully, but these errors were encountered: