Skip to content

Commit

Permalink
Make incremental compilation aware of synthesized mirrors
Browse files Browse the repository at this point in the history
A product mirror needs to be resynthesized if any class parameter changes, and
a sum mirror needs to be resynthesized if any child of the sealed type changes,
but previously this did not reliably work because the dependency recording in
ExtractDependencies was unaware of mirrors.

Instead of making ExtractDependencies aware of mirrors, we solve this by
directly recording the dependencies when the mirror is synthesized, this way we
can be sure to always correctly invalidate users of mirrors, even if the
synthesized mirror type is not present in the AST at phase ExtractDependencies.

This is the first time that we record dependencies outside of the
ExtractDependencies phase, in the future we should see if we can extend this
mechanism to record more dependencies during typechecking to make incremental
compilation more robust (e.g. by keeping track of symbols looked up by macros).

Eventually, we might even want to completely get rid of the ExtractDependencies
phase and record all dependencies on the fly if it turns out to be faster.
  • Loading branch information
smarter committed Jul 29, 2023
1 parent 54e2f59 commit ec59c31
Show file tree
Hide file tree
Showing 13 changed files with 85 additions and 4 deletions.
9 changes: 9 additions & 0 deletions compiler/src/dotty/tools/dotc/CompilationUnit.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,20 @@ class CompilationUnit protected (val source: SourceFile) {
/** List of all comments present in this compilation unit */
var comments: List[Comment] = Nil

/** This is used to record dependencies to invalidate during incremental
* compilation, but only if `ctx.runZincPhases` is true.
*/
val depRecorder: sbt.DependencyRecorder = sbt.DependencyRecorder()

/** Suspends the compilation unit by thowing a SuspendException
* and recording the suspended compilation unit
*/
def suspend()(using Context): Nothing =
assert(isSuspendable)
// Clear references to symbols that may become stale. No need to call
// `depRecorder.sendToZinc()` since all compilation phases will be rerun
// when this unit is unsuspended.
depRecorder.clear()
if !suspended then
if (ctx.settings.XprintSuspension.value)
report.echo(i"suspended: $this")
Expand Down
13 changes: 10 additions & 3 deletions compiler/src/dotty/tools/dotc/sbt/ExtractDependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class ExtractDependencies extends Phase {

override def run(using Context): Unit = {
val unit = ctx.compilationUnit
val rec = DependencyRecorder()
val rec = unit.depRecorder
val collector = ExtractDependenciesCollector(rec)
collector.traverse(unit.tpdTree)

Expand Down Expand Up @@ -422,8 +422,15 @@ class DependencyRecorder {
case (usedName, scopes) =>
cb.usedName(className, usedName.toString, scopes)
classDependencies.foreach(recordClassDependency(cb, _))
_usedNames.clear()
_classDependencies.clear()
clear()

/** Clear all state. */
def clear(): Unit =
_usedNames.clear()
_classDependencies.clear()
lastOwner = NoSymbol
lastDepSource = NoSymbol
_responsibleForImports = NoSymbol

/** Handles dependency on given symbol by trying to figure out if represents a term
* that is coming from either source code (not necessarily compiled in this compilation
Expand Down
19 changes: 18 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Synthesizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import ast.Trees.genericEmptyTree
import annotation.{tailrec, constructorOnly}
import ast.tpd._
import Synthesizer._
import sbt.ExtractDependencies.*
import sbt.ClassDependency
import xsbti.api.DependencyContext._

/** Synthesize terms for special classes */
class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
Expand Down Expand Up @@ -458,7 +461,14 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
val reason = s"it reduces to a tuple with arity $arity, expected arity <= $maxArity"
withErrors(i"${defn.PairClass} is not a generic product because $reason")
case MirrorSource.ClassSymbol(pre, cls) =>
if cls.isGenericProduct then makeProductMirror(pre, cls, None)
if cls.isGenericProduct then
if ctx.runZincPhases then
// The mirror should be resynthesized if the constructor of the
// case class `cls` changes. See `sbt-test/source-dependencies/mirror-product`.
val rec = ctx.compilationUnit.depRecorder
rec.addClassDependency(cls, DependencyByMemberRef)
rec.addUsedName(cls.primaryConstructor)
makeProductMirror(pre, cls, None)
else withErrors(i"$cls is not a generic product because ${cls.whyNotGenericProduct}")
case Left(msg) =>
withErrors(i"type `$mirroredType` is not a generic product because $msg")
Expand All @@ -478,6 +488,13 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
val clsIsGenericSum = cls.isGenericSum(pre)

if acceptableMsg.isEmpty && clsIsGenericSum then
if ctx.runZincPhases then
// The mirror should be resynthesized if any child of the sealed class
// `cls` changes. See `sbt-test/source-dependencies/mirror-sum`.
val rec = ctx.compilationUnit.depRecorder
rec.addClassDependency(cls, DependencyByMemberRef)
rec.addUsedName(cls, includeSealedChildren = true)

val elemLabels = cls.children.map(c => ConstantType(Constant(c.name.toString)))

def internalError(msg: => String)(using Context): Unit =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
case class MyProduct(x: Int)
10 changes: 10 additions & 0 deletions sbt-test/source-dependencies/mirror-product/Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import scala.deriving.Mirror
import scala.compiletime.erasedValue

transparent inline def foo[T](using m: Mirror.Of[T]): Int =
inline erasedValue[m.MirroredElemTypes] match
case _: (Int *: EmptyTuple) => 1
case _: (Int *: String *: EmptyTuple) => 2

@main def Test =
assert(foo[MyProduct] == 2)
1 change: 1 addition & 0 deletions sbt-test/source-dependencies/mirror-product/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
scalaVersion := sys.props("plugin.scalaVersion")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
case class MyProduct(x: Int, y: String)
7 changes: 7 additions & 0 deletions sbt-test/source-dependencies/mirror-product/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
> compile

# change the case class constructor
$ copy-file changes/MyProduct.scala MyProduct.scala

# Both MyProduct.scala and Test.scala should be recompiled, otherwise the assertion will fail
> run
2 changes: 2 additions & 0 deletions sbt-test/source-dependencies/mirror-sum/Sum.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
sealed trait Sum
case class Child1() extends Sum
12 changes: 12 additions & 0 deletions sbt-test/source-dependencies/mirror-sum/Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import scala.deriving.Mirror
import scala.compiletime.erasedValue

object Test:
transparent inline def foo[T](using m: Mirror.Of[T]): Int =
inline erasedValue[m.MirroredElemLabels] match
case _: ("Child1" *: EmptyTuple) => 1
case _: ("Child1" *: "Child2" *: EmptyTuple) => 2

def main(args: Array[String]): Unit =
assert(foo[Sum] == 2)

4 changes: 4 additions & 0 deletions sbt-test/source-dependencies/mirror-sum/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
scalaVersion := sys.props("plugin.scalaVersion")
// Use more precise invalidation, otherwise the reference to `Sum` in
// Test.scala is enough to invalidate it when a child is added.
ThisBuild / incOptions ~= { _.withUseOptimizedSealed(true) }
3 changes: 3 additions & 0 deletions sbt-test/source-dependencies/mirror-sum/changes/Sum.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
sealed trait Sum
case class Child1() extends Sum
case class Child2() extends Sum
7 changes: 7 additions & 0 deletions sbt-test/source-dependencies/mirror-sum/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
> compile

# Add a child
$ copy-file changes/Sum.scala Sum.scala

# Both Sum.scala and Test.scala should be recompiled, otherwise the assertion will fail
> run

0 comments on commit ec59c31

Please sign in to comment.