Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,10 @@ private[inc] abstract class IncrementalCommon(
val removedClasses = classNames(removedSrcs)
val dependentOnRemovedClasses = removedClasses.flatMap(previous.memberRef.internal.reverse)
val modifiedClasses = classNames(modifiedSrcs)
val invalidatedClasses = removedClasses ++ dependentOnRemovedClasses ++ modifiedClasses
val inheritanceInvalidations = modifiedClasses.flatMap(previous.inheritance.internal.reverse)
Copy link
Member

Choose a reason for hiding this comment

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

I haven't carefully studied how the bug is occurring, or why this would fix it, but in theory the inheritance relationship is already captured as dependency relationship in Zinc. So if changing

abstract class A

to

trait A

is causing undercompilation, I wonder if we need to enrich the invalidation, specifically when the parent type changes its definition type, as if the signature has changed. The fact that this fixes the bug, but doesn't include information about the trait-vs-class I suspect that it's also might be source of potential problem.

Copy link
Author

Choose a reason for hiding this comment

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

The bug in #598 isn't a typical undercompilation, it's crashing during the incremental compilation. This crash is sort of due to undercompilation in that if you include the right file in the incremental compilation it doesn't occur (thus why this "fix" fixes it), but it's also possible Zinc would figure it out in a later cycle if it didn't crash.

The failure mode if you back out this "fix" and run test I added "recompile subclass when superclass changes from abstract class to trait" is long so I've put it in a gist: https://gist.github.com/jackkoenig/ce190c77fda3e571d43bbe6d6534a327

Copy link
Member

Choose a reason for hiding this comment

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

I see, so:

  1. class B extends trait A, compile both.
  2. change A to abstract class A, and compile only A.scala, scala.tools.nsc.backend.jvm.BTypes crashes with "Bad/Invalid superClass" error.
  3. compiling both B.scala and A.scala would fix the error, but always invalidating all child classes creates overcompilation, since recompilation would happen even for body code changes.

As a workaround, I wonder if we can allow Bad/Invalid superClass error only for the compile cycle 1.


val invalidatedClasses =
removedClasses ++ dependentOnRemovedClasses ++ modifiedClasses ++ inheritanceInvalidations

val byProduct = changes.removedProducts.flatMap(previous.produced)
val byLibraryDep = changes.libraryDeps.flatMap(previous.usesLibrary)
Expand Down
67 changes: 67 additions & 0 deletions zinc/src/test/scala/sbt/inc/IncrementalCompilerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,73 @@ class IncrementalCompilerSpec extends BaseCompilerSpec {
}
}

it should "recompile subclass when superclass changes from abstract class to trait" in withTmpDir {
tmp =>
val project = VirtualSubproject(tmp.toPath / "p1")
val comp = project.setup.createCompiler()
try {
// Initial sources: A is an abstract class, B extends A
val aAbstractClass =
"""abstract class A
|object Test { new B }""".stripMargin

val aAsTrait =
"""trait A
|object Test { new B }""".stripMargin

val bSource = "class B extends A"

val f1 = StringVirtualFile("A.scala", aAbstractClass)
val f1Modified = StringVirtualFile("A.scala", aAsTrait)
val f2 = StringVirtualFile("B.scala", bSource)

val res1 = comp.compile(f1, f2)
val res2 = comp.compile(f1Modified, f2)

assert(recompiled(res1, res2) == Set("A", "B", "Test"))
} finally {
comp.close()
}
}

it should "not recompile all files in a cycle for non-API changes" in withTmpDir { tmp =>
val project = VirtualSubproject(tmp.toPath / "p1")
val comp = project.setup.createCompiler()
try {
// Create a cyclic dependency: X references Y, Y references X
val xSource =
"""class X {
| def x: Y = null
| def impl = 1
|}""".stripMargin
// Only implementation change, no API change
val xSourceModified =
"""class X {
| def x: Y = null
| def impl = 2
|}""".stripMargin

val ySource =
"""class Y {
| def y: X = null
|}""".stripMargin

val zSource = "class Z { def z = 1 }"

val fX = StringVirtualFile("X.scala", xSource)
val fXModified = StringVirtualFile("X.scala", xSourceModified)
val fY = StringVirtualFile("Y.scala", ySource)
val fZ = StringVirtualFile("Z.scala", zSource)

val res1 = comp.compile(fX, fY, fZ)
val res2 = comp.compile(fXModified, fY, fZ)

assert(recompiled(res1, res2) == Set("X"))
} finally {
comp.close()
}
}

it should "not throw NullPointerException when passing -Xshow-phases to scalac" in withTmpDir {
tmp =>
val comp = ProjectSetup.simple(
Expand Down
Loading