-
Notifications
You must be signed in to change notification settings - Fork 1.7k
In front end, do not relax override rules when overridden parameter is covariant #31596
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Comments
Based on an IRL discussion with Lasse: Considering this issue again, and again, it seems less disruptive, and less confusing, if we do allow the compilers to generate forwarders (and hence we consider The argument that actually killed the idea of requiring explicit forwarders for me was that a plain forwarding declaration, @leafpetersen, I suspect that it's OK with you to allow these forwarders to be generated implicitly; @lrhn, I believe that I heard you talk in favor of this approach earlier today? I'm sorry about going back and forth on this topic, but I think we should make the right decision rather than just fixing one decision that seemed to be in favor at some point. If so, it is no longer a 'front-end-missing-error'; it may be a no-op, but I don't know the implementation details well enough so that may be wrong. |
@leafpetersen, @lrhn: Do you agree that we should allow generating these forwarders after all (and get them specified, too, of course)? |
Reluctantly, yes. class C {
void foo(int x) { ... }
}
abstract class I<T> {
void foo(T x);
}
class D extends C implements I<int> {
} it just looks like it should work. It'll be very hard to explain to users why it doesn't. |
lgtm |
OK, cool! So, Paul @stereotype441, this is no more a "front-end-missing-error". Maybe this means that there is nothing to do, because dartdevc already generates these forwarders? I won't close the issue because I might be wrong, but I suspect that it can be closed. |
@eernstg I'm not 100% sure what the implementations currently do. I'll create a language_2 test verifying the behavior that I believe we want, and I'll send it to you, @leafpetersen, and @lrhn for review. Then I'll decide what to do with this bug based on whether that test passes or not :) |
Cool! |
Ok, here's what I've found:
I've created a CL with a test demonstrating these behaviors: https://dart-review.googlesource.com/#/c/sdk/+/31750 All of these problems should theoretically take care of themselves when these tools are transitioned over to use the unified front end. I think the presence of the failing test should be a sufficient reminder to ensure that everything works properly once the various tools are transitioned over to the unified front end, so unless someone objects, I'll close this bug once the aforementioned CL lands. |
One thing we hadn't discussed so far was the source of the signature of the induced forwarding method. I've argued in the CL that the forwarder should get the signature from the forwardee (in the example it is Apart from the technical reasons for doing so (given in the CL comment), the conceptual justification could be that the forwarder is a "generated corrected version" of the forwardee |
@eernstg I'm a bit skeptical about using the less specific type for the interface of
I guess the counter-argument is that the user gets more accurate static errors if we choose the @stereotype441 brings up a very good point from my perspective: if |
Let me see if I can say what this means, such that we can check that we understand it in a similar manner: A nominal subtype is introduced by a subtype related syntactic clause ( So with I'm not 100% sure how I could map that to the choice of typing One interpretation that does come to mind is that, with However, that's exactly what developers must expect when they see So I can see the temptation to ascribe the type On the contrary, I think we have good reasons for ascribing the type The most prominent one is simply the semantics of the construct that we're discussing. Here we have it, explicitly: class A {}
class B extends A {}
class C {
void f(B x, B y) {}
}
abstract class I1 {
void f(covariant A x, B y);
}
class D extends C implements I1 {
void f(B x, B y) { super.f(x, y); }
} This example basically shows the code that we're generating. The core conceptual reason why I think we should take this into account when we ascribe a type to Another reason is that the type of the forwardee may well yield the solution to a computational task that we are otherwise rejecting to perform in a compiler/analyzer: class A {}
class B extends A {}
class C {
void f(B x, [B y]) {}
}
abstract class I1 {
void f(covariant A x);
}
abstract class I2 {
void f(A x, B y);
}
class D extends C implements I1, I2 {} In this example, we couldn't use the signature from If we can obtain a well-formed class by inheriting some method (forget covariance for a minute) then the signature of that method must be a correct override for all signatures of So, taking covariance into account, if we need to implicitly generate a forwarder for such an (otherwise) inherited method, it will work to adopt the method signature from the forwardee (amended with covariance as needed, simply because the forwarder is placed in
I actually think that the method signature from the actual forwardee fits quite nicely for "uses the implementation of C to implement I1", just like the following: class C {
void f(Object o) {}
}
abstract class I1 {
void f(int i);
}
class D extends C implements I1 {}
main() => new D().f('Hello, int!'); // Fine! In this situation we also adopt the signature from the inherited implementation, even though it isn't identical to the signature in the class that Maybe the general phrase should be "uses the implementation of
If class A {}
class B extends A {}
abstract class C {
void f(B x, B y);
}
abstract class I1 {
void f(covariant A x, B y);
}
abstract class D extends C implements I1 {} In the interface of If
I don't think we can make that distinction: We may obtain implementations from direct or indirect superclasses (including the ones obtained via mixins), but the interface of the superclass is based on the interfaces of its superclasses in turn, and the interface of the superclass is just one more superinterface, which contributes to the interface of the class in focus. So whenever we have an implementation it will contribute to the interface of the class just like all the non-implementation method signatures that we get from the I think this means I wasn't really convinced. ;-) |
tl;dr - Let the synthetic forwarder be an implementation-only thing, with
the signature of the implementation it forwards to + covariant, and keep it
out of the interface so the D.foo signature is inherited normally from the
super-interfaces
On Wed, Jan 3, 2018 at 7:43 PM, Leaf Petersen ***@***.***> wrote:
@eernstg <https://github.com/eernstg> I'm a bit skeptical about using the
less specific type for the interface of D.f there.
- On a theoretical level, all other things being equal I'd like to
have the property that nominal subtypes are structural subtypes as well.
Choosing the C.f interface breaks this.
It's not the C.f *interface*, it's the C.f *implementation*, with its
signature updated to be covariant.
That might be a little too subtle, but I believe it's a good trade-off
between rejecting programs that would function and not doing too much magic.
I really, really hope it won't happen much in practice (because I hope
covariant won't happen much, except as generics).
The alternative, as I see it, is to say that a non-covariant implementation
parameter cannot be considered a valid implementation of a covariant
interface parameter unless the non-covariant implementation would be a
valid implementation of a corresponding non-covariant interface parameter
too.
- Obviously covariant overrides in general break this, but still, all
other things being equal I lean away from deviating here.
I would definitely not want to change the parameter types of the
*implementation*. Making the parameter covariant is stretching it already,
and if it wasn't for my cases like example above, I'd probably have voted
for just rejecting the program.
- From a user intent perspective, my sense is that if someone writes extends
C implements I1, they mean either "uses the implementation of C to
implement I1" or "implements the combination of the interfaces of C and
I1", but probably not "uses the implementation of C to implement the
interface of C, ignoring the interface of I1".
... but you *are* using the implementation of C, so that is what the
concrete implementation should be.
I'm quite adamant that the type of the forwarder should be based on the
implementation it forwards too.
Now, we can discuss whether the synthetic forwarder should affect the
*interface.*
In the case of
class D extends C implements I {}
that would mean that ...
- If the synthetic `void foo(covariant B, B)` forwarder method is
considered as declared on D, then that will be the D interface signature of
foo.
- If we ignore the synthetic forwarder, then we have to pick the most
specific of `void foo(B, B)` from C and `void foo(covariant A, B)` from I.
That's the one from I.
Which is an interesting point in any case: If you have two interfaces
provide the same function, one with a covariant argument and the other
being more specific, would we synthesize the resulting interface signature
to be most-specific + covariant? We probably should (so type overrides and
covariant overrides are orthogonal, and can come from different
super-interfaces and both affect the resulting interface, we just won't
combine types from different interfaces).
Another example:
class A {
void foo(num x) {};
}
class B extends A {
void foo(covariant num x); // auto-forwarder here? Probably yes. (No
interface issue here, but still needs to work).
}
- It's not quite as black and white as that presentation makes it
sound - it's possible that the user is not really interested in the version
of f from I1 and only gets it by happenstance, but this seems less
likely than either of the first two interpretations.
- It's slightly odd that promoting to the super type would give you
fewer errors. We already have that with covariant generics and covariant
overrides in general, but I still feel like erring on the side of sanity
where possible.
I guess the counter-argument is that the user gets more accurate static
errors if we choose the C.f signature, since the actual implementation
signature is in fact that of C.f. And I suppose another way of phrasing
the "user intent" argument is that the user intends instances of D to be
usable at I1, but may want D to have a more precise signature - that's
kind of the point of covariant overrides.
@stereotype441 <https://github.com/stereotype441> brings up a very good
point from my perspective: if C is abstract, then presumably we would
choose the I1.f signature?
If the class is abstract, then we probably won't be adding any synthetic
forwarders. In that case the interface won't include the synthetic
forwarder (obviously). I think it's a good argument that the interface
should not change even if we do add the forwarder, so I vote to keep the
forwarder out of the interface computation. It's an *implementation* detail
only (like abstract methods are interface details only and doesn't affect
implementation).
If not, on what basis do we not? And if so, then I find it quite odd that
we would choose a different signature depending on whether or not C is
abstract. I don't know if it's possible, but it seems like a good principle
that choosing the interface of a class should be done based on
super-interfaces, not based on implementations.
The interface of a class should be inherited from super-interfaces (and if
a method exists in more than one super-interface, pick the most specific
one, and (new) inherit covariance from any superinterface) except where the
class itself declares a member.
If it does declare a member, that member's signature always win (but must
be a valid override of all corresponding super-interface members).
So, in this case, if we consider the synthetic forwarder as not declared by
the class, the interface works, and the new implementation must still be
valid for that interface.
That means:
class C {
void foo(num x) {}
}
abstract class I {
void foo(covariant String x);
}
class D extends C with I {} // COMPILE-TIME ERROR!
We can't find a signature for D.foo from the super-interfaces, and even if
we could, a forwarded like `void foo(covariant num x) => super.foo(x);`
will not be a valid implementation of `void foo(covariant String)` from the
I interface.
|
I haven't read the whole thing yet, but I agree completely on this part. |
Since the type given to the forwarder isn't directly visible to the user, I think it might be beneficial to focus the discussion on the compile-time and runtime behaviors we want to see, and then once we agree on that decide the best way to implement the desired behaviors in kernel and front end. Considering a slightly expanded example: class I0 {}
class A {}
class B extends A implements I0 {}
class B2 extends A {}
class C {
void f(B x, B y) {}
}
abstract class C2 {
void f(B x, B y);
}
abstract class I1 {
void f(covariant A x, B y);
}
class D extends C implements I1 {}
abstract class D2 extends C2 implements I1 {}
void test1(D d, B2 b2) { d.f(b2, null); }
void test2(D2 d, B2 b2) { d2.f(b2, null); }
void test3(D d) { void Function(B2, B) f = d.f; }
void test4(D2 d) { void Function(B2, B) f = d2.f; }
class Test5 extends D {
void f(covariant I0 x, B y) {}
}
class Test6 extends D {
void f(covariant B2 x, B y) {}
}
void test7() { new D().f(new A(), null); }
void test8() { print(new D().f is void Function(Object, B)); } I think these are the behavioral questions we need to answer (along with my proposed answers): (1) At compile time, when type checking, how do we account for a forwarder when checking what the static type of the argument should be assignable to? In other words, in the example above, should (2) At compile time, when type checking, how do we account for a forwarder when determining the static type of a tear-off? In other words, in the example above, should (3) At compile time, when checking whether an override is valid, how do we account for a forwarder in the class being overridden? In other words, should class (4) What types should be checked at runtime when a forwarder is executed? In other words, in the example above, should (5) What runtime type should be given to a tear-off of a forwarder? In other words, what should be printed by Are there other behavioral questions we need to answer? I believe the current implementation has all of these proposed behaviors. But I now see that they were achieved in a counterintuitive way: I gave the forwarder the signature |
I agree with Paul's summary. I believe if I understand Lasse's response correctly, that he and I are in agreement. By transitivity... presumably Lasse agrees with Paul's summary. Lasse, if not, please speak up. I think Erik is still not convinced. For me, the uniformity between the abstract and concrete case is conclusive: I believe we can, and should, synthesize the class interface based on super-interfaces independent of whether there is a concrete implementation backing the interface. The fact that there is a concrete implementation, and possibly a forwarder, is an implementation detail. |
tl;dr Paul, I think the generated forwarder should be taken into account; otherwise we agree on everything rd;lt Apparently, you all agree that the generated forwarder should be ignored when computing the class interface. I prefer to take it into account. The reason why I want to take the generated forwarder into account is that it has observable consequences when it is added. I mentioned already long ago that the reified type of You seem to think that it is "cleaner" or "more correct" to ignore the generated forwarder, possibly because that amounts to "better encapsulation" or "abstractness" or something like that. However, given that the generated forwarder matters for the meaning of the program, it's a completely standard causal chain: There is a declaration of So here are the consequences for your examples, Paul: class I0 {}
class A {}
class B extends A implements I0 {}
class B2 extends A {}
class C {
void f(B x, B y) {}
}
abstract class C2 {
void f(B x, B y);
}
abstract class I1 {
void f(covariant A x, B y);
}
class D extends C implements I1 {
void f(B x, B y) => super.f(x, y); // Generated, and taken into account.
}
abstract class D2 extends C2 implements I1 {
// No generated forwarders, interface has `void Function(A, B)` for `f`.
}
void test1(D d, B2 b2) { d.f(b2, null); }
void test2(D2 d, B2 b2) { d2.f(b2, null); }
void test3(D d) { void Function(B2, B) f = d.f; }
void test4(D2 d) { void Function(B2, B) f = d2.f; }
class Test5 extends D {
void f(covariant I0 x, B y) {}
}
class Test6 extends D {
void f(covariant B2 x, B y) {}
}
void test7() { new D().f(new A(), null); }
void test8() { print(new D().f is void Function(Object, B)); }
For For
So we agree everywhere except in I think this reflects the situation quite well: When the generated forwarder is taken into account, the static analysis is improved --- because the static information is more honest about the situation. So, again, it all comes down to whether it is actually an implementation detail. ;-) |
On Thu, Jan 4, 2018 at 8:23 PM, Paul Berry ***@***.***> wrote:
Since the type given to the forwarder isn't directly visible to the user,
I think it might be beneficial to focus the discussion on the compile-time
and runtime behaviors we want to see, and then once we agree on that decide
the best way to implement the desired behaviors in kernel and front end.
Considering a slightly expanded example:
class I0 {}class A {}class B extends A implements I0 {}class B2 extends A {}class C {
void f(B x, B y) {}
}abstract class C2 {
void f(B x, B y);
}abstract class I1 {
void f(covariant A x, B y);
}class D extends C implements I1 {}abstract class D2 extends C2 implements I1 {}void test1(D d, B2 b2) { d.f(b2, null); }void test2(D2 d, B2 b2) { d2.f(b2, null); }void test3(D d) { void Function(B2, B) f = d.f; }void test4(D2 d) { void Function(B2, B) f = d2.f; }class Test5 extends D {
void f(covariant I0 x, B y) {}
}class Test6 extends D {
void f(covariant B2 x, B y) {}
}void test7() { new D().f(new A(), null); }void test8() { print(new D().f is void Function(Object, B)); }
I think these are the behavioral questions we need to answer (along with
my proposed answers):
(1) At compile time, when type checking, how do we account for a forwarder
when checking what the static type of the argument should be assignable to?
We probably ignore it. The type of the forwarder is the type of the
*implementation*, it only affects static typing when it comes to
super-calls.
The *static type* of D.f is "void Function(covariant A, B)". That's the
"most specific" type of f among the immediate super-interfaces of D (C and
I1) with covariant on A because at least one of the f's of the
superinterfaces has that (it's the same one as the one we chose the
parameter type from, but that's coincidental).
If we had:
class X extends D {
void f(A x, B y) => super.f(x, y);
}
*then* it would be a compile-time error because the static type of super.f
is "void Function(covariant B, B)".
That's the only place you access the super-*implementation*, and that's
where the actual type matters.
(Remember that we don't treat covariant functions as more capable than
their known static type, which is why we disallow:
D x = ...;
x.f(new Object());
as well, even though we "know" that the call will succeed-and-throw at
runtime).
In other words, in the example above, should test1 have a compile-time
error?
No. The static type of `D.f` is `void Function(covariant A, B)`, as
inherited from I1.
My proposal is: concreteness vs abstractness should not affect the answer,
so we should give the same answer for test2 and test1.
Agree.
In test2 there is no forwarder, so the inherited interface for D2.f has
type (A, B) -> void, therefore test2 should not have a compile-time
error. Therefore test1 should not have a compile time error either.
(2) At compile time, when type checking, how do we account for a forwarder
when determining the static type of a tear-off?
We don't. The static type of a tear-off is the static type of the interface
method.
Again, there will be a discrepancy if we use super:
class X extends D {
void f(A x, B x) {
var superF = super.f;
f(x, y); // Compile-time error, static type of superF is void
Function(B, B).
}
}
In other words, in the example above, should test3 have a compile-time
error? I would propose a similar resolution here: in test4 there is no
forwarder, so the inherited interface for D2.f has type (A, B) -> void,
which is assignable to (B2, B) -> void, therefore test4 should not have a
compile time error. Therefore test3 should not have a compile time error
either.
Agree.
(3) At compile time, when checking whether an override is valid, how do we
account for a forwarder in the class being overridden?
Again we ignore it, that depends entirely on the interface signature.
Since the interface signature is `void Function(A, B)`, that is the thing
we need to override. The important check is whether a non-abstract class
implements its interface.
In other words, should class Test5 have a compile time error? How about
Test6? My proposal is: synthetic forwarders shouldn't participate in the
test for what constitutes a valid override; instead we follow the usual
rule that a parameter that has (or inherits) the "covariant" keyword needs
to have a type that is assignable to the type of the corresponding
overridden parameter in every non-synthetic overridden method (including
transitive overrides). The non-synthetic overridden methods are C.f and
I1.f; these declare a first parameter type of B and A respectively;
therefore both Test5 and Test6 are in error, because I0 is not assignable
to A, and B2 is not assignable to B.
Agree.
(4) What types should be checked at runtime when a forwarder is executed?
If the call is to `D.f`, and the arguments statically match (A, B), then
there should be no call-site checks (if they don't match, there might be
static down-casts to A or B).
Then we hit an implementation with a first argument of `covariant B`, which
will throw on non-B arguments.
In other words, in the example above, should test7 have a runtime error?
Yes.
I think that to ensure soundness, the types to be checked must eventually
come from the method that the forwarder forwards to, which in this case is
C.f, having type (B, B) -> void. Therefore test7 should have a runtime
error.
(5) What runtime type should be given to a tear-off of a forwarder?
It's a covariant function, so it's `void Function(Object, B)`.
In other words, what should be printed by test8? My proposal is that
since the forwarder's x parameter is covariant, the tear-off should have
runtime type (Object, B) -> void, so test8 should print true.
Agree.
Are there other behavioral questions we need to answer?
I am not sure we have consciously and deliberately agreed that a covariant
implementation with type `void Function(covariant B, B)` is a valid
*implementation* of the interface signature `void Function(covariant A, B)`.
We probably should, I expect the rule to be that, for a non-abstract class,
a member implementation must have a signature that is a valid override for
the interface signature of the corresponding member.
That is the case here, so it would pass that test, and indeed, the code
"works" (even if it's known to throw in a number of cases).
I believe the current implementation has all of these proposed behaviors.
But I now see that they were achieved in a counterintuitive way: I gave the
forwarder the signature (covariant A, B) -> void in the kernel
representation, which ensured (1), (2), (3), and (5) but not (4). I then
"fixed" (4) by requiring the back ends to look up the method being
forwarded to when determining what type checks to perform in the forwarder.
An alternative implementation would have been to give the forwarder the
signature (covariant B, B) -> void, which would have ensured (3), (4),
and (5), and then to fix (1) and (2) by changing the front end's type
checking code so that it ignores forwarders (this would be similar to
Lasse's suggestion that we keep the forwarder "out of the interface"). I am
certainly open to changing the implementation (though there are some
technical difficulties due to limitations in the kernel ClassHierarchy
API). But I want to make sure that we agree on the behaviors we want first.
I stay by my idea, and I think it matches the tests you wrote above.
The forwarder will be a real methods, it can be torn off and accessed using
super, so it matters.
Now, for the comparable example where the covariance is already on the
implementation:
class C {
void f(covariant B x) {}
}
abstract class I {
void f(A x);
}
class D extends C implements I {} // D.f has static type void Function(A).
Do we agree that this is a valid program (it would not be one if the
argument was non-covariant).
There is no forwarder, and the implementation only implements the interface
due to covariance.
|
On Fri, Jan 5, 2018 at 3:48 PM, Erik Ernst <notifications@github.com> wrote:
...
So we agree everywhere except in test1 and test3 where, arguably, the
static analysis is better when we take the generated forwarder into account
when computing the class interface.
I think this reflects the situation quite well: When the generated
forwarder is taken into account, the static analysis is improved ---
because the static information is more honest about the situation. So,
again, it all comes down to whether it is actually an implementation
detail. ;-)
It is a good point that the type is more precise if we take the forwarder
into account, but that comes back to the example I gave above:
class C {
void f(covariant B x) {}
}
abstract class I {
void f(A x);
}
class D extends C implements I {} // D.f has static type void Function(A).
Here there is no forwarder. We probably want to accept the program. If not,
then we should't just not introduce a forwarder and reject the original
program too because the implementation only accepts B and the interface
expects an A.
(We'd still need a forwarder in the case where the implementation is valid
for the interface, but it isn't covariant, but there will be no problem
with the interface there because the implementation must then be more
precise than the interface).
I think the original example should match this example wrt. interface.
Otherwise we get an interface change by moving the covariant marker from C
to I, which is unintuitive.
So, either reject both programs, or get the same interface - which means
not letting the presence or absence of an implementation forwarder affect
the interface.
|
I've written a rough draft of an approach to interfaces in Dart (which uses It's basically the same stuff as usual, but explicitly flattened, and then I believe it shows that we could obtain the more precise typing of the As far as I can understand (from discussing the basic setup with Johnni) Finally, I expect any change in this area to break a very small amount of |
OK, I know you hate this, and we don't have the time. So let's stick to the model that everybody except I preferred, and then we can keep these other things in mind for a later time. ;-) |
Ok, based on this I'm going to move ahead with https://dart-review.googlesource.com/c/sdk/+/31750 as is, however I will update the comments in it to reflect the discussion here. Then I'll follow up with another CL containing the test cases we've been discussing in this thread, and my understanding of what we've agreed upon; I'll send that CL to Lasse, Erik, and Leaf for review so that if I've misunderstood something you can correct me. |
Change-Id: I34abe6993e0dc85d1234878c91ce735139b9cb47 Reviewed-on: https://dart-review.googlesource.com/31750 Reviewed-by: Leaf Petersen <leafp@google.com> Commit-Queue: Paul Berry <paulberry@google.com>
Change-Id: Ib67f8ae239b89bf56efc059054b6430e27a0e66f Reviewed-on: https://dart-review.googlesource.com/34922 Commit-Queue: Paul Berry <paulberry@google.com> Reviewed-by: Erik Ernst <eernst@google.com>
What's the status of this? |
I'm not sure, and at this point it's been long enough since I worked on it that I would have to take a fresh look. Since I'm not on the front end project anymore, probably someone on the front end team should take a look. |
We pass language_2/issue31596_test. Analyzer fails with
I presume that means the CFE issue is fixed, but I can't follow the discussion in this issue well enough to confirm that. Can we verify fixed? |
@leafpetersen wrote:
I just needed to look into this area. Cf. this comment, we decided that it is not correct to inherit an implementation whose parameters are not covariant into a class where the same declaration would only be correct when it is taken into account that the parameters would be covariant-by-declaration implicitly. So the following is an error as indicated in the comment: class C {
void f(int x) {}
}
abstract class I {
void f(covariant num x);
}
class D extends C implements I {} // Error. As of b31566b, the analyzer reports a compile-time error, but CFE doesn't. However, it is allowed to inherit an implementation whose parameters are not covariant into a class where the same declaration would only be correct when it is taken into account that the parameters would be covariant-by-class. class C {
void f(int x) {}
}
abstract class I<T> {
void f(T x);
}
class D extends C implements I<int> {} // OK. With this example, same commit, the analyzer does not report any compile-time errors, and neither does CFE. The dynamic type error is detected by So the status is that |
The tests related to this issue are broken and I'm trying to clean them up. The
I'm happy to update the tests, but I need some help from the language lawyers on the current set of expectations: class I0 {}
class A {}
class B extends A implements I0 {}
class B2 extends A {}
class C {
void f(B x) {}
}
abstract class I {
void f(covariant A x);
}
// analyzer rejects classes D and E on the grounds that `C.f` isn't a valid override of `I.f`, but all other configurations accept them.
class D extends C implements I {}
class E extends D {
void test() {
I0 i0 = null;
B2 b2 = null;
// What should the type of `super.f` be?
// * The test expects `superF` to have type (B) -> void below.
// * Printing `super.f.runtimeType` says that it's (Object) -> void.
// * Parts of the test behave like it's (A) -> void (or (covariant A) -> void)).
super.f(i0); // We expect this to succeed but it fails on every configuration because `I0` is not assignable to `A`.
super.f(b2); // We expect a compile-time error because `B2` is not assignable to `B`, but this passes on CFE.
var superF = super.f;
// What is the inferred type of `superF`?
// The test expects (B) -> void, but see the discussion above regarding `super.f`.
// In particular, are the following `true` or `false`? (We expect them all to be true but these fail on all configurations except fasta.)
// * superF is void Function(B)
// * superF is! void Function(I0)
// * superF is! void Function(A)
// * superF is! void Function(Object)
// These are analogous to the `super.f` invocations:
superF(i0);
superF(b2);
}
} The following tests also fail on the 6 analyzer/DDC configuration:
(The first 3 also happen to fail on |
About Class Class So the test as a whole isn't so useful as long as it's all wrong, and it doesn't look like it is intended to have that property. ;-) But it could be changed to express the expectation that And then it could be useful to have a variant of the original test as well, testing that it is allowed to have a very similar situation, as long as it has covariance-by-class where the current one has covariance-by-declaration (I added comments in the code using brackets, after the part that I'm commenting on): class I0 {}
class A {}
class B extends A implements I0 {}
class B2 extends A {}
class C {
void f(B x) {}
}
abstract class I<X> {
void f(X x);
}
class D extends C implements I<B> {}
class E extends D {
void test() {
I0 i0 = null;
B2 b2 = null;
// What should the type of `super.f` be?
// [`void f(B)`; the parameter is covariant-by-class, but that's not visible in the signature]
// * The test expects `superF` to have type (B) -> void below.
// * Printing `super.f.runtimeType` says that it's (Object) -> void.
// [That's correct, that reified parameter type is `Object`; but there's a generated check for `B`.]
// * Parts of the test behave like it's (A) -> void (or (covariant A) -> void)).
// [It does have type `void Function(A)`, and `void Function(S)` for _any_ `S`,
// but the dynamic check will make an invocation with argument `A()` throw.]
super.f(i0); // We expect this to succeed but it fails on every configuration because `I0` is not assignable to `A`.
// [The static parameter type is `B`, so (until NNBD) `IO` is assignable. No compile-time error.]
super.f(b2); // We expect a compile-time error because `B2` is not assignable to `B`, but this passes on CFE.
// [Right, this should be a compile-time error now.]
var superF = super.f;
// What is the inferred type of `superF`?
// [The static type of the tear-off preserves the declared type: `void Function(B)`.]
// The test expects (B) -> void, but see the discussion above regarding `super.f`.
// In particular, are the following `true` or `false`? (We expect them all to be true but these fail on all configurations except fasta.)
// * superF is void Function(B)
// * superF is! void Function(I0)
// * superF is! void Function(A)
// * superF is! void Function(Object)
// [This is the dynamic type (`void Function(Object)`): true, false, false, false.]
// These are analogous to the `super.f` invocations:
superF(i0);
// [OK.]
superF(b2);
// [Compile-time error.]
}
} |
Change-Id: I02d56402aa09c1c9563929ab3792743b8fe7e97d Bug: #31596 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/116085 Commit-Queue: Mayank Patke <fishythefish@google.com> Reviewed-by: Erik Ernst <eernst@google.com> Auto-Submit: Mayank Patke <fishythefish@google.com>
Related to #46389 |
Closing: The language team decided that we would generate the forwarding methods that are required for soundness reasons (or whatever implementation a tool may use, forwarding methods is just one possible implementation). It is currently being implemented (cf. #47072, with spec changes in dart-lang/language#1833). With this change, the tests |
Consider the following code:
As pointed out in #31580 (comment), the class
D
should be rejected at compile time because its implementation off
has typevoid Function(B, B)
, which is not a valid override type for the member type off
in the interface ofD
,void Function(covariant A, B)
.I have not yet implemented the override checking rules in the front end (I will soon). This bug serves as a reminder that the case above should result in an error.
The text was updated successfully, but these errors were encountered: