Skip to content

Make incremental compilation aware of synthesized mirrors #18310

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

Merged
merged 3 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 8 additions & 3 deletions compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -676,11 +676,16 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {

// In the Scala2 ExtractAPI phase we only extract annotations that extend
// StaticAnnotation, but in Dotty we currently pickle all annotations so we
// extract everything (except annotations missing from the classpath which
// we simply skip over, and inline body annotations which are handled above).
// extract everything, except:
// - annotations missing from the classpath which we simply skip over
// - inline body annotations which are handled above
// - the Child annotation since we already extract children via
// `api.ClassLike#childrenOfSealedClass` and adding this annotation would
// lead to overcompilation when using zinc's
// `IncOptions#useOptimizedSealed`.
s.annotations.foreach { annot =>
val sym = annot.symbol
if sym.exists && sym != defn.BodyAnnot then
if sym.exists && sym != defn.BodyAnnot && sym != defn.ChildAnnot then
annots += apiAnnotation(annot)
}

Expand Down
101 changes: 66 additions & 35 deletions compiler/src/dotty/tools/dotc/sbt/ExtractDependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -108,29 +108,6 @@ object ExtractDependencies {
report.error(em"Internal error in the incremental compiler while compiling ${ctx.compilationUnit.source}: $msg", pos)
}

/** An object that maintain the set of used names from within a class */
private final class UsedNamesInClass {
private val _names = new mutable.HashMap[Name, EnumSet[UseScope]]
def names: collection.Map[Name, EnumSet[UseScope]] = _names

def update(name: Name, scope: UseScope): Unit = {
val scopes = _names.getOrElseUpdate(name, EnumSet.noneOf(classOf[UseScope]))
scopes.add(scope)
}

override def toString(): String = {
val builder = new StringBuilder
names.foreach { case (name, scopes) =>
builder.append(name.mangledString)
builder.append(" in [")
scopes.forEach(scope => builder.append(scope.toString))
builder.append("]")
builder.append(", ")
}
builder.toString()
}
}

/** Extract the dependency information of a compilation unit.
*
* To understand why we track the used names see the section "Name hashing
Expand All @@ -144,7 +121,7 @@ private class ExtractDependenciesCollector(rec: DependencyRecorder) extends tpd.

private def addMemberRefDependency(sym: Symbol)(using Context): Unit =
if (!ignoreDependency(sym)) {
rec.addUsedName(sym, UseScope.Default)
rec.addUsedName(sym)
// packages have class symbol. Only record them as used names but not dependency
if (!sym.is(Package)) {
val enclOrModuleClass = if (sym.is(ModuleVal)) sym.moduleClass else sym.enclosingClass
Expand Down Expand Up @@ -202,7 +179,7 @@ private class ExtractDependenciesCollector(rec: DependencyRecorder) extends tpd.
for sel <- selectors if !sel.isWildcard do
addImported(sel.name)
if sel.rename != sel.name then
rec.addUsedRawName(sel.rename, UseScope.Default)
rec.addUsedRawName(sel.rename)
case exp @ Export(expr, selectors) =>
val dep = expr.tpe.classSymbol
if dep.exists && selectors.exists(_.isWildcard) then
Expand Down Expand Up @@ -322,7 +299,7 @@ private class ExtractDependenciesCollector(rec: DependencyRecorder) extends tpd.
val traverser = new TypeDependencyTraverser {
def addDependency(symbol: Symbol) =
if (!ignoreDependency(symbol) && symbol.is(Sealed)) {
rec.addUsedName(symbol, UseScope.PatMatTarget)
rec.addUsedName(symbol, includeSealedChildren = true)
}
}
traverser.traverse(tpe)
Expand All @@ -347,26 +324,80 @@ class DependencyRecorder {
*/
def usedNames: collection.Map[Symbol, UsedNamesInClass] = _usedNames

/** Record a reference to the name of `sym` used according to `scope`
* from the current non-local enclosing class.
/** Record a reference to the name of `sym` from the current non-local
* enclosing class.
*
* @param includeSealedChildren See documentation of `addUsedRawName`.
*/
def addUsedName(sym: Symbol, scope: UseScope)(using Context): Unit =
addUsedRawName(sym.zincMangledName, scope)
def addUsedName(sym: Symbol, includeSealedChildren: Boolean = false)(using Context): Unit =
addUsedRawName(sym.zincMangledName, includeSealedChildren)

/** Record a reference to `name` used according to `scope`
* from the current non-local enclosing class.
/** Record a reference to `name` from the current non-local enclosing class (aka, "from class").
*
* Most of the time, prefer to sue `addUsedName` which takes
* Most of the time, prefer to use `addUsedName` which takes
* care of name mangling.
*
* Zinc will use this information to invalidate the current non-local
* enclosing class if something changes in the set of definitions named
* `name` among the possible dependencies of the from class.
*
* @param includeSealedChildren If true, the addition or removal of children
* to a sealed class called `name` will also
* invalidate the from class.
* Note that this only has an effect if zinc's
* `IncOptions.useOptimizedSealed` is enabled,
* otherwise the addition or removal of children
* always lead to invalidation.
*
* TODO: If the compiler reported to zinc all usages of
* `SymDenotation#{children,sealedDescendants}` (including from macro code),
* we should be able to turn `IncOptions.useOptimizedSealed` on by default
* safely.
*/
def addUsedRawName(name: Name, scope: UseScope)(using Context): Unit = {
def addUsedRawName(name: Name, includeSealedChildren: Boolean = false)(using Context): Unit = {
val fromClass = resolveDependencySource
if (fromClass.exists) {
val usedName = _usedNames.getOrElseUpdate(fromClass, new UsedNamesInClass)
usedName.update(name, scope)
usedName.update(name, includeSealedChildren)
}
}

// The two possible value of `UseScope`. To avoid unnecessary allocations,
// we use vals here, but that means we must be careful to never mutate these sets.
private val DefaultScopes = EnumSet.of(UseScope.Default)
private val PatMatScopes = EnumSet.of(UseScope.Default, UseScope.PatMatTarget)

/** An object that maintain the set of used names from within a class */
final class UsedNamesInClass {
/** Each key corresponds to a name used in the class. To understand the meaning
* of the associated value, see the documentation of parameter `includeSealedChildren`
* of `addUsedRawName`.
*/
private val _names = new mutable.HashMap[Name, DefaultScopes.type | PatMatScopes.type]

def names: collection.Map[Name, EnumSet[UseScope]] = _names

private[DependencyRecorder] def update(name: Name, includeSealedChildren: Boolean): Unit = {
if (includeSealedChildren)
_names(name) = PatMatScopes
else
_names.getOrElseUpdate(name, DefaultScopes)
Copy link
Member

Choose a reason for hiding this comment

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

is this going to reuse the closure?

Copy link
Member Author

Choose a reason for hiding this comment

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

The by-name closure isn't guaranteed to be reused, it's up to the JVM

}

override def toString(): String = {
val builder = new StringBuilder
names.foreach { case (name, scopes) =>
builder.append(name.mangledString)
builder.append(" in [")
scopes.forEach(scope => builder.append(scope.toString))
builder.append("]")
builder.append(", ")
}
builder.toString()
}
}


private val _classDependencies = new mutable.HashSet[ClassDependency]

def classDependencies: Set[ClassDependency] = _classDependencies
Expand Down
2 changes: 2 additions & 0 deletions sbt-test/source-dependencies/useOptimizedSealed/Sealed.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
sealed trait Sealed
class Child1 extends Sealed
3 changes: 3 additions & 0 deletions sbt-test/source-dependencies/useOptimizedSealed/Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Test {
val s: Sealed = new Child1
}
29 changes: 29 additions & 0 deletions sbt-test/source-dependencies/useOptimizedSealed/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
scalaVersion := sys.props("plugin.scalaVersion")

ThisBuild / incOptions ~= { _.withUseOptimizedSealed(true) }

import sbt.internal.inc.Analysis
import complete.DefaultParsers._

// Reset compiler iterations, necessary because tests run in batch mode
val recordPreviousIterations = taskKey[Unit]("Record previous iterations.")
recordPreviousIterations := {
val log = streams.value.log
CompileState.previousIterations = {
val previousAnalysis = (previousCompile in Compile).value.analysis.asScala
previousAnalysis match {
case None =>
log.info("No previous analysis detected")
0
case Some(a: Analysis) => a.compilations.allCompilations.size
}
}
}

val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.")

checkIterations := {
val expected: Int = (Space ~> NatBasic).parsed
val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations
assert(expected == actual, s"Expected $expected compilations, got $actual")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
sealed trait Sealed
class Child1 extends Sealed
class Child2 extends Sealed
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
sealed trait Sealed
class Child1 extends Sealed
class Child2 extends Sealed
class Child3 extends Sealed
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Test {
def foo(x: Sealed): Int = x match
case _: Child1 => 1
case _: Child2 => 1

val s: Sealed = new Child1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// This is necessary because tests are run in batch mode
object CompileState {
@volatile var previousIterations: Int = -1
}
24 changes: 24 additions & 0 deletions sbt-test/source-dependencies/useOptimizedSealed/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Compile Sealed.scala and Test.scala
> compile
> recordPreviousIterations

# Add an extra children to Sealed
$ copy-file changes/Sealed1.scala Sealed.scala

# Only Sealed.scala needs to be recompiled because Test.scala does not
# match on a value of type `Sealed`.
> compile
> checkIterations 1

> clean
$ copy-file changes/Test1.scala Test.scala
> compile
> recordPreviousIterations

# Add an extra children to Sealed again
$ copy-file changes/Sealed2.scala Sealed.scala

# Test.scala will be recompiled in a second round because it matches
# on a value of type `Sealed`.
> compile
> checkIterations 2