Skip to content
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

Refine overriding pairs in RefChecks #12982

Merged
merged 8 commits into from
Jul 5, 2021
Merged

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Jun 29, 2021

We now exclude a pair of overriding / overridden symbols that
have a common subclass parent of the base class only under the
additional condition that their signatures also match when seen
as members of the parent class.

#12828 shows a case where this matters:

trait Foo[A]:
  def foo(x: A): Unit

trait Bar[A] extends Foo[A]:
  def foo(x: A & String): Unit = println(x.toUpperCase)

object Baz extends Bar[Int] // error overriding foo: incompatible type

@main def run() = Baz.foo(42)

When checking Baz, there is a common subclass parent (namely Bar),
but the signatures of the two foo definitions as seen from Bar are
different, so we cannot assume that the pair has already been checked.

Fixes #12828

We now exclude a pair of overriding / overridden symbols that
have a common subclass parent of the base class only under the
additional condition that their signatures also match.

scala#12828 shows a case where this matters:
```scala
trait Foo[A]:
  def foo(x: A): Unit

trait Bar[A] extends Foo[A]:
  def foo(x: A & String): Unit = println(x.toUpperCase)

object Baz extends Bar[Int] // error overriding foo: incompatible type

@main def run() = Baz.foo(42)
```
When checking `Baz`, there is a common subclass parent (namely `Bar`),
but the signatures of the two `foo` definitions as seen from `Bar` are
different, so we cannot assume that the pair has already been checked.

Fixes scala#12828
@odersky
Copy link
Contributor Author

odersky commented Jun 29, 2021

test performance please

@dottybot
Copy link
Member

performance test scheduled: 1 job(s) in queue, 0 running.

@smarter
Copy link
Member

smarter commented Jun 29, 2021

Before complicating OverridingPairs more I think it's worth considering how what we're doing differ from Scala 2 which is known to be more correct than us (#7551), namely we know that https://github.com/lampepfl/dotty/blob/master/tests/run/i5094.scala is compiling when it shouldn't because the final method SO is override by the method in SOIO. This was fixed in Scala 2 by scala/scala@cc55bd9 which says:

Subclassing on traits does not imply suffixing for their linearisations

And indeed if we change OverridingPairs#isSubParent to also check !parent.is(Trait), then i5094.scala stops compiling, and if we also disable the if (member.owner != clazz) { ... block in RefChecks#checkOverride, #12828 is also fixed (and the issue doesn't reappear when replacing the traits by abstract classes). By contrast the fix in this PR doesn't stop i5094.scala from compiling, so it's not clear to me it's the right way forward.

@dottybot
Copy link
Member

Performance test finished successfully:

Visit https://dotty-bench.epfl.ch/12982/ to see the changes.

Benchmarks is based on merging with master (d96a500)

@odersky
Copy link
Contributor Author

odersky commented Jun 30, 2021

test performance please

@dottybot
Copy link
Member

performance test scheduled: 1 job(s) in queue, 0 running.

@dottybot
Copy link
Member

Performance test finished successfully:

Visit https://dotty-bench.epfl.ch/12982/ to see the changes.

Benchmarks is based on merging with master (f34a3bb)

@odersky
Copy link
Contributor Author

odersky commented Jun 30, 2021

@smarter Unfortunately, the situation between Scala 2 and 3 is not comparable since Scala 3 is signature based and Scala 2 isn't. So the algorithms are different. The problem with trait ordering in linearization can be fixed easily with another test in OverridingPairs.

But it turns out that the duplicate checks in checkAllOverride that looked redundant are not. They re-introduce the problem. Specifically, this one:

      // return if we already checked this combination elsewhere
      if (member.owner != clazz) {
        def deferredCheck = member.is(Deferred) || !other.is(Deferred)
        def subOther(s: Symbol) = s derivesFrom other.owner
        def subMember(s: Symbol) = s derivesFrom member.owner

        if (subOther(member.owner) && deferredCheck)
          //Console.println(infoString(member) + " shadows1 " + infoString(other) " in " + clazz);//DEBUG
          return

Here, if we make foo in Foo concrete, the condition kicks in and the pair is discarded even though the signatures are different. Hence, the test compiles and we get a crash at runtime.

But if Ii remove the early returnn in checkAllOverrides, stdlib213 does not compile anymore. It complains as follows:

[error] -- Error: /__w/dotty/dotty/community-build/community-projects/stdLib213/src/library/scala/collection/MapView.scala:19:6 
[error] 19 |trait MapView[K, +V]
[error]    |      ^
[error]    |trait MapView inherits conflicting members:
[error]    |  method concat in trait IterableOps of type [B >: (K, V)](suffix: IterableOnce[B]): scala.collection.View[B]  and
[error]    |  method concat in trait MapOps of type [V2 >: V](suffix: IterableOnce[(K, V2)]): scala.collection.View[(K, V2)]
[error]    |(Note: this can be resolved by declaring an override in trait MapView.);
[error]    |other members with override errors are:: method ++, method ++:

At first glance, this looks like a correct error, and might need a fix stdlib213.

EDIT: It's a correct error only if we decide that two members override if their signatures match. The last commits go into another direction: Decree that there's not a real override since the full parameter types do not match, and suppress the error message and also any bridge generations.

Remove illogical conditions that suppressed certain checks. Replace them
by a separate condition that certain override checks are suppressed if the
types of the members do not match (i.e. it's just the signatures that match
but not the types). In that case, we have arguably overshot with declaring
an override. Bridge generation is also suppressed in this case. Scala 2 behaves
effectively the same way, by never taking signatures into account.
Refchecks now decouples overriding from signature matching. A signature match is in essence a pre-check that two members might override, but if the two members are methods we also need to compare their parameter types. This has three consequences:

 1. Before emitting an override error, test that parameter types match. If not, we have a false override and no error should be reported.
 2. Before emitting a bridge, do the same test and omit the generation if the test fails
 3. When checking whether a class is fully implemented, check that any implementation also agrees completely in its parameter types.
@odersky
Copy link
Contributor Author

odersky commented Jun 30, 2021

Refchecks now decouples overriding from signature matching. A signature match is in essence a pre-check that two members might override, but if the two members are methods we also need to compare their parameter types. This has three consequences:

  1. Before emitting an override error, test that parameter types match. If not, we have a false override and no error should be reported.
  2. Before emitting a bridge, do the same test and omit the generation if the test fails
  3. When checking whether a class is fully implemented, check that any implementation also agrees completely in its parameter types.

@odersky
Copy link
Contributor Author

odersky commented Jun 30, 2021

test performance please

@dottybot
Copy link
Member

performance test scheduled: 1 job(s) in queue, 0 running.

@dottybot
Copy link
Member

Performance test finished successfully:

Visit https://dotty-bench.epfl.ch/12982/ to see the changes.

Benchmarks is based on merging with master (79fae19)

@odersky
Copy link
Contributor Author

odersky commented Jul 1, 2021

That was quite a rabbit hole.

@odersky odersky requested a review from smarter July 1, 2021 07:37
@odersky odersky assigned odersky and smarter and unassigned odersky Jul 1, 2021
@@ -1021,7 +1021,7 @@ object Denotations {
* erasure (see i8615b, i9109b), Erasure takes care of adding any necessary
* bridge to make this work at runtime.
*/
def matchesLoosely(other: SingleDenotation)(using Context): Boolean =
def matchesLoosely(other: SingleDenotation, alwaysCompareParams: Boolean = false)(using Context): Boolean =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be called alwaysCompareTypes since Type#matches can also check the result type if we're after erasure.

Comment on lines 327 to 328
* If not we treat them as not a real override and don't issue certain
* error messages. Also, bridges are not generated in this case.
Copy link
Member

@smarter smarter Jul 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not skip all error messages instead of only certain ones? Currently it looks like we still emit overrideAccessError and overrideTargetNameError but if these methods are not true override then I don't see why we need to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact it's just the comment that is wrong. All errors go to overrideError which does the check whether it's a true override.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All errors go to overrideError which does the check whether it's a true override

In that case, why not immediately check for false overrides at the top of checkOverride before doing any of the checks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the false overrides check is expensive and override checks are common while override errors are rare.

@smarter smarter assigned odersky and unassigned smarter Jul 2, 2021
@odersky odersky assigned smarter and unassigned odersky Jul 2, 2021
@bishabosha bishabosha requested a review from smarter July 5, 2021 09:09
@smarter smarter linked an issue Jul 5, 2021 that may be closed by this pull request
Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, great to see this on more solid footing!

@smarter smarter merged commit 40c587f into scala:master Jul 5, 2021
@smarter smarter deleted the fix-12828 branch July 5, 2021 11:48
@@ -0,0 +1,8 @@
-- Error: tests/neg/i12828.scala:7:7 -----------------------------------------------------------------------------------
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file should be removed

@Kordyjan Kordyjan added this to the 3.0.2 milestone Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants