From 9d844b3eea5f786409d77e9ecb2cc2f5ae832965 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Tue, 22 Jun 2021 15:00:36 +0200 Subject: [PATCH 1/8] Add failing test for #11861 --- .../source-dependencies/inline-rec/A.scala | 5 ++++ .../source-dependencies/inline-rec/B.scala | 5 ++++ .../source-dependencies/inline-rec/C.scala | 3 +++ .../source-dependencies/inline-rec/build.sbt | 25 +++++++++++++++++++ .../inline-rec/changes/A1.scala | 5 ++++ .../inline-rec/changes/C1.scala | 3 +++ .../source-dependencies/inline-rec/dbg.sbt | 3 +++ .../inline-rec/project/CompileState.scala | 4 +++ .../project/DottyInjectedPlugin.scala | 11 ++++++++ sbt-test/source-dependencies/inline-rec/test | 7 ++++++ 10 files changed, 71 insertions(+) create mode 100644 sbt-test/source-dependencies/inline-rec/A.scala create mode 100644 sbt-test/source-dependencies/inline-rec/B.scala create mode 100644 sbt-test/source-dependencies/inline-rec/C.scala create mode 100644 sbt-test/source-dependencies/inline-rec/build.sbt create mode 100644 sbt-test/source-dependencies/inline-rec/changes/A1.scala create mode 100644 sbt-test/source-dependencies/inline-rec/changes/C1.scala create mode 100644 sbt-test/source-dependencies/inline-rec/dbg.sbt create mode 100644 sbt-test/source-dependencies/inline-rec/project/CompileState.scala create mode 100644 sbt-test/source-dependencies/inline-rec/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/source-dependencies/inline-rec/test diff --git a/sbt-test/source-dependencies/inline-rec/A.scala b/sbt-test/source-dependencies/inline-rec/A.scala new file mode 100644 index 000000000000..6bf7ee29cda2 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec/A.scala @@ -0,0 +1,5 @@ +object A { + inline def callInline: Int = inlinedInt + + inline def inlinedInt: Int = 23 +} diff --git a/sbt-test/source-dependencies/inline-rec/B.scala b/sbt-test/source-dependencies/inline-rec/B.scala new file mode 100644 index 000000000000..2b4bd09c6e23 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec/B.scala @@ -0,0 +1,5 @@ +class B { + def main(args: Array[String]): Unit = { + assert(A.callInline == C.expected) + } +} diff --git a/sbt-test/source-dependencies/inline-rec/C.scala b/sbt-test/source-dependencies/inline-rec/C.scala new file mode 100644 index 000000000000..3b32f0026167 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec/C.scala @@ -0,0 +1,3 @@ +object C { + def expected: Int = 23 +} diff --git a/sbt-test/source-dependencies/inline-rec/build.sbt b/sbt-test/source-dependencies/inline-rec/build.sbt new file mode 100644 index 000000000000..1036709ccfc1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec/build.sbt @@ -0,0 +1,25 @@ +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") +} diff --git a/sbt-test/source-dependencies/inline-rec/changes/A1.scala b/sbt-test/source-dependencies/inline-rec/changes/A1.scala new file mode 100644 index 000000000000..32941f30d8ab --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec/changes/A1.scala @@ -0,0 +1,5 @@ +object A { + inline def callInline: Int = inlinedInt + + inline def inlinedInt: Int = 47 +} diff --git a/sbt-test/source-dependencies/inline-rec/changes/C1.scala b/sbt-test/source-dependencies/inline-rec/changes/C1.scala new file mode 100644 index 000000000000..377d5add6ca8 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec/changes/C1.scala @@ -0,0 +1,3 @@ +object C { + def expected: Int = 47 +} diff --git a/sbt-test/source-dependencies/inline-rec/dbg.sbt b/sbt-test/source-dependencies/inline-rec/dbg.sbt new file mode 100644 index 000000000000..f9a522c9e610 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec/dbg.sbt @@ -0,0 +1,3 @@ +logLevel := Level.Debug +incOptions in ThisBuild ~= { _.withApiDebug(true) } +incOptions in ThisBuild ~= { _.withRelationsDebug(true) } diff --git a/sbt-test/source-dependencies/inline-rec/project/CompileState.scala b/sbt-test/source-dependencies/inline-rec/project/CompileState.scala new file mode 100644 index 000000000000..078db9c7bf56 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt-test/source-dependencies/inline-rec/project/DottyInjectedPlugin.scala b/sbt-test/source-dependencies/inline-rec/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..fb946c4b8c61 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec/project/DottyInjectedPlugin.scala @@ -0,0 +1,11 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion") + ) +} diff --git a/sbt-test/source-dependencies/inline-rec/test b/sbt-test/source-dependencies/inline-rec/test new file mode 100644 index 000000000000..1716f03c6c82 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec/test @@ -0,0 +1,7 @@ +> compile +> recordPreviousIterations +# Force recompilation of B because A.inlinedInt, called by A.callInline, has changed +$ copy-file changes/A1.scala A.scala +> compile +# 1 to recompile A, then 1 more to recompile B due to A.inlinedInt change +> checkIterations 2 From 641a8bb23649aa30f10ee876400761deb589226d Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Tue, 22 Jun 2021 16:21:30 +0200 Subject: [PATCH 2/8] fix #11861, mix in nested inline def or val --- .../src/dotty/tools/dotc/sbt/ExtractAPI.scala | 130 ++++++++++++++---- .../src/dotty/tools/dotc/sbt/package.scala | 5 + .../inline-rec-change-inline/A.scala | 5 + .../inline-rec-change-inline/B.scala | 5 + .../inline-rec-change-inline/C.scala | 3 + .../inline-rec-change-inline/build.sbt | 25 ++++ .../inline-rec-change-inline/changes/B1.scala | 5 + .../project/CompileState.scala | 4 + .../project/DottyInjectedPlugin.scala | 11 ++ .../inline-rec-change-inline/test | 9 ++ .../inline-rec-change-param/A.scala | 5 + .../inline-rec-change-param/B.scala | 5 + .../inline-rec-change-param/C.scala | 3 + .../inline-rec-change-param/build.sbt | 25 ++++ .../inline-rec-change-param/changes/B1.scala | 5 + .../project/CompileState.scala | 4 + .../project/DottyInjectedPlugin.scala | 11 ++ .../inline-rec-change-param/test | 9 ++ .../inline-rec-change-res-transparent/A.scala | 5 + .../inline-rec-change-res-transparent/B.scala | 5 + .../inline-rec-change-res-transparent/C.scala | 3 + .../build.sbt | 25 ++++ .../changes/B1.scala | 5 + .../project/CompileState.scala | 4 + .../project/DottyInjectedPlugin.scala | 11 ++ .../inline-rec-change-res-transparent/test | 9 ++ .../inline-rec-change-res/A.scala | 5 + .../inline-rec-change-res/B.scala | 5 + .../inline-rec-change-res/C.scala | 3 + .../inline-rec-change-res/build.sbt | 25 ++++ .../inline-rec-change-res/changes/B1.scala | 5 + .../project/CompileState.scala | 4 + .../project/DottyInjectedPlugin.scala | 11 ++ .../inline-rec-change-res/test | 9 ++ .../inline-rec-change-typaram/A.scala | 5 + .../inline-rec-change-typaram/B.scala | 5 + .../inline-rec-change-typaram/C.scala | 3 + .../inline-rec-change-typaram/build.sbt | 25 ++++ .../changes/B1.scala | 5 + .../project/CompileState.scala | 4 + .../project/DottyInjectedPlugin.scala | 11 ++ .../inline-rec-change-typaram/test | 9 ++ .../inline-rec-deep/A.scala | 5 + .../inline-rec-deep/B.scala | 5 + .../inline-rec-deep/C.scala | 5 + .../inline-rec-deep/D.scala | 3 + .../inline-rec-deep/build.sbt | 25 ++++ .../inline-rec-deep/changes/C1.scala | 5 + .../project/CompileState.scala | 4 + .../project/DottyInjectedPlugin.scala | 11 ++ .../source-dependencies/inline-rec-deep/test | 10 ++ .../inline-rec-mut/A.scala | 8 ++ .../inline-rec-mut/B.scala | 8 ++ .../inline-rec-mut/C.scala | 3 + .../inline-rec-mut/build.sbt | 25 ++++ .../inline-rec-mut/changes/B1.scala | 9 ++ .../inline-rec-mut/project/CompileState.scala | 4 + .../project/DottyInjectedPlugin.scala | 11 ++ .../source-dependencies/inline-rec-mut/test | 11 ++ .../inline-rec-val/A.scala | 5 + .../inline-rec-val/B.scala | 3 + .../inline-rec-val/build.sbt | 25 ++++ .../inline-rec-val/changes/A1.scala | 5 + .../inline-rec-val/project/CompileState.scala | 4 + .../project/DottyInjectedPlugin.scala | 11 ++ .../source-dependencies/inline-rec-val/test | 7 + .../source-dependencies/inline-rec/B.scala | 4 +- .../source-dependencies/inline-rec/C.scala | 3 - .../inline-rec/changes/C1.scala | 3 - .../source-dependencies/inline-rec/dbg.sbt | 3 - 70 files changed, 656 insertions(+), 39 deletions(-) create mode 100644 sbt-test/source-dependencies/inline-rec-change-inline/A.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-inline/B.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-inline/C.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-inline/build.sbt create mode 100644 sbt-test/source-dependencies/inline-rec-change-inline/changes/B1.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-inline/project/CompileState.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-inline/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-inline/test create mode 100644 sbt-test/source-dependencies/inline-rec-change-param/A.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-param/B.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-param/C.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-param/build.sbt create mode 100644 sbt-test/source-dependencies/inline-rec-change-param/changes/B1.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-param/project/CompileState.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-param/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-param/test create mode 100644 sbt-test/source-dependencies/inline-rec-change-res-transparent/A.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res-transparent/B.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res-transparent/C.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res-transparent/build.sbt create mode 100644 sbt-test/source-dependencies/inline-rec-change-res-transparent/changes/B1.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res-transparent/project/CompileState.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res-transparent/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res-transparent/test create mode 100644 sbt-test/source-dependencies/inline-rec-change-res/A.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res/B.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res/C.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res/build.sbt create mode 100644 sbt-test/source-dependencies/inline-rec-change-res/changes/B1.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res/project/CompileState.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-res/test create mode 100644 sbt-test/source-dependencies/inline-rec-change-typaram/A.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-typaram/B.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-typaram/C.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-typaram/build.sbt create mode 100644 sbt-test/source-dependencies/inline-rec-change-typaram/changes/B1.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-typaram/project/CompileState.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-typaram/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/source-dependencies/inline-rec-change-typaram/test create mode 100644 sbt-test/source-dependencies/inline-rec-deep/A.scala create mode 100644 sbt-test/source-dependencies/inline-rec-deep/B.scala create mode 100644 sbt-test/source-dependencies/inline-rec-deep/C.scala create mode 100644 sbt-test/source-dependencies/inline-rec-deep/D.scala create mode 100644 sbt-test/source-dependencies/inline-rec-deep/build.sbt create mode 100644 sbt-test/source-dependencies/inline-rec-deep/changes/C1.scala create mode 100644 sbt-test/source-dependencies/inline-rec-deep/project/CompileState.scala create mode 100644 sbt-test/source-dependencies/inline-rec-deep/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/source-dependencies/inline-rec-deep/test create mode 100644 sbt-test/source-dependencies/inline-rec-mut/A.scala create mode 100644 sbt-test/source-dependencies/inline-rec-mut/B.scala create mode 100644 sbt-test/source-dependencies/inline-rec-mut/C.scala create mode 100644 sbt-test/source-dependencies/inline-rec-mut/build.sbt create mode 100644 sbt-test/source-dependencies/inline-rec-mut/changes/B1.scala create mode 100644 sbt-test/source-dependencies/inline-rec-mut/project/CompileState.scala create mode 100644 sbt-test/source-dependencies/inline-rec-mut/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/source-dependencies/inline-rec-mut/test create mode 100644 sbt-test/source-dependencies/inline-rec-val/A.scala create mode 100644 sbt-test/source-dependencies/inline-rec-val/B.scala create mode 100644 sbt-test/source-dependencies/inline-rec-val/build.sbt create mode 100644 sbt-test/source-dependencies/inline-rec-val/changes/A1.scala create mode 100644 sbt-test/source-dependencies/inline-rec-val/project/CompileState.scala create mode 100644 sbt-test/source-dependencies/inline-rec-val/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/source-dependencies/inline-rec-val/test delete mode 100644 sbt-test/source-dependencies/inline-rec/C.scala delete mode 100644 sbt-test/source-dependencies/inline-rec/changes/C1.scala delete mode 100644 sbt-test/source-dependencies/inline-rec/dbg.sbt diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index 6e198bbeada9..6c65c8f29c9b 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -599,7 +599,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { // an inline def in every class that extends its owner. To avoid this we // could store the hash as an annotation when pickling an inline def // and retrieve it here instead of computing it on the fly. - val inlineBodyHash = treeHash(inlineBody) + val inlineBodyHash = treeHash(inlineBody, inlineSym = s) annots += marker(inlineBodyHash.toString) } @@ -620,14 +620,110 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { * it should stay the same across compiler runs, compiler instances, * JVMs, etc. */ - def treeHash(tree: Tree): Int = + def treeHash(tree: Tree, inlineSym: Symbol): Int = import scala.util.hashing.MurmurHash3 + import core.Constants.* + + val seenInlines = mutable.HashSet.empty[Symbol] + + if inlineSym ne NoSymbol then + seenInlines += inlineSym // do not hash twice a recursive def + + def nameHash(n: Name, initHash: Int): Int = + val h = + if n.isTermName then + MurmurHash3.mix(initHash, TermNameHash) + else + MurmurHash3.mix(initHash, TypeNameHash) + + // The hashCode of the name itself is not stable across compiler instances + MurmurHash3.mix(h, n.toString.hashCode) + end nameHash + + def typeHash(tp: Type, initHash: Int): Int = + // Go through `apiType` to get a value with a stable hash, it'd + // be better to use Murmur here too instead of relying on + // `hashCode`, but that would essentially mean duplicating + // https://github.com/sbt/zinc/blob/develop/internal/zinc-apiinfo/src/main/scala/xsbt/api/HashAPI.scala + // and at that point we might as well do type hashing on our own + // representation. + var h = initHash + tp match + case ConstantType(c) => + h = constantHash(c, h) + case TypeBounds(lo, hi) => + h = MurmurHash3.mix(h, apiType(lo).hashCode) + h = MurmurHash3.mix(h, apiType(hi).hashCode) + case tp => + h = MurmurHash3.mix(h, apiType(tp).hashCode) + h + end typeHash + + def constantHash(c: Constant, initHash: Int): Int = + var h = MurmurHash3.mix(initHash, c.tag) + c.tag match + case NullTag => + // No value to hash, the tag is enough. + case ClazzTag => + h = typeHash(c.typeValue, h) + case _ => + h = MurmurHash3.mix(h, c.value.hashCode) + h + end constantHash + + /**An inline method that calls another inline method will eventually inline the call + * at a non-inline callsite, in this case if the implementation of the nested call + * changes, then the callsite will have a different API, we should hash the definition + */ + def inlineReferenceHash(ref: Symbol, rhs: Tree, initHash: Int): Int = + var h = initHash + + def paramssHash(paramss: List[List[Symbol]], initHash: Int): Int = paramss match + case Nil :: paramss1 => + paramssHash(paramss1, MurmurHash3.mix(initHash, EmptyParamHash)) + case params :: paramss1 => + var h = initHash + val paramsIt = params.iterator + while paramsIt.hasNext do + val param = paramsIt.next + h = nameHash(param.name, h) + h = typeHash(param.info, h) + if param.is(Inline) then + h = MurmurHash3.mix(h, InlineParamHash) // inline would change the generated code + paramssHash(paramss1, h) + case Nil => + initHash + end paramssHash + + h = paramssHash(ref.paramSymss, h) + h = typeHash(ref.info.finalResultType, h) + positionedHash(rhs, h) + end inlineReferenceHash + + def err(what: String, elem: Any, pos: Positioned, initHash: Int): Int = + internalError(i"Don't know how to produce a stable hash for $what", pos.sourcePos) + MurmurHash3.mix(initHash, elem.toString.hashCode) def positionedHash(p: ast.Positioned, initHash: Int): Int = + var h = initHash + p match case p: WithLazyField[?] => p.forceIfLazy case _ => + + p match + case ref: RefTree @unchecked => + val sym = ref.symbol + if sym.is(Inline, butNot = Param) && !seenInlines.contains(sym) then + seenInlines += sym // dont re-enter hashing this ref + sym.defTree match + case defTree: ValOrDefDef => + h = inlineReferenceHash(sym, defTree.rhs, h) + case _ => + h = err(i"inline method reference `${ref.name}`", ref.name, ref, h) + case _ => + // FIXME: If `p` is a tree we should probably take its type into account // when hashing it, but producing a stable hash for a type is not trivial // since the same type might have multiple representations, for method @@ -635,12 +731,11 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { // in Zinc that generates hashes from that, if we can reliably produce // stable hashes for types ourselves then we could bypass all that and // send Zinc hashes directly. - val h = MurmurHash3.mix(initHash, p.productPrefix.hashCode) + h = MurmurHash3.mix(h, p.productPrefix.hashCode) iteratorHash(p.productIterator, h) end positionedHash def iteratorHash(it: Iterator[Any], initHash: Int): Int = - import core.Constants._ var h = initHash while it.hasNext do it.next() match @@ -649,30 +744,11 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { case xs: List[?] => h = iteratorHash(xs.iterator, h) case c: Constant => - h = MurmurHash3.mix(h, c.tag) - c.tag match - case NullTag => - // No value to hash, the tag is enough. - case ClazzTag => - // Go through `apiType` to get a value with a stable hash, it'd - // be better to use Murmur here too instead of relying on - // `hashCode`, but that would essentially mean duplicating - // https://github.com/sbt/zinc/blob/develop/internal/zinc-apiinfo/src/main/scala/xsbt/api/HashAPI.scala - // and at that point we might as well do type hashing on our own - // representation. - val apiValue = apiType(c.typeValue) - h = MurmurHash3.mix(h, apiValue.hashCode) - case _ => - h = MurmurHash3.mix(h, c.value.hashCode) + h = constantHash(c, h) case n: Name => - // The hashCode of the name itself is not stable across compiler instances - h = MurmurHash3.mix(h, n.toString.hashCode) + h = nameHash(n, h) case elem => - internalError( - i"Don't know how to produce a stable hash for `$elem` of unknown class ${elem.getClass}", - tree.sourcePos) - - h = MurmurHash3.mix(h, elem.toString.hashCode) + h = err(i"`$elem` of unknown class ${elem.getClass}", elem, tree, h) h end iteratorHash @@ -691,6 +767,6 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { // annotated @org.junit.Test). api.Annotation.of( apiType(annot.tree.tpe), // Used by sbt to find tests to run - Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree).toString))) + Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree, inlineSym = NoSymbol).toString))) } } diff --git a/compiler/src/dotty/tools/dotc/sbt/package.scala b/compiler/src/dotty/tools/dotc/sbt/package.scala index d5f9a289c800..99a6b97bddef 100644 --- a/compiler/src/dotty/tools/dotc/sbt/package.scala +++ b/compiler/src/dotty/tools/dotc/sbt/package.scala @@ -5,6 +5,11 @@ import dotty.tools.dotc.core.Symbols.Symbol import dotty.tools.dotc.core.NameOps.stripModuleClassSuffix import dotty.tools.dotc.core.Names.Name +inline val TermNameHash = 1987 // 300th prime +inline val TypeNameHash = 1993 // 301st prime +inline val EmptyParamHash = 1997 // 302nd prime +inline val InlineParamHash = 1999 // 303rd prime + extension (sym: Symbol) def constructorName(using Context) = diff --git a/sbt-test/source-dependencies/inline-rec-change-inline/A.scala b/sbt-test/source-dependencies/inline-rec-change-inline/A.scala new file mode 100644 index 000000000000..d06100a22564 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-inline/A.scala @@ -0,0 +1,5 @@ +object A { + + inline def callInline: Any = B.inlinedAny("yyy") + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-inline/B.scala b/sbt-test/source-dependencies/inline-rec-change-inline/B.scala new file mode 100644 index 000000000000..61e61a620957 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-inline/B.scala @@ -0,0 +1,5 @@ +object B { + + inline def inlinedAny(x: String): x.type = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-inline/C.scala b/sbt-test/source-dependencies/inline-rec-change-inline/C.scala new file mode 100644 index 000000000000..e6511852afa1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-inline/C.scala @@ -0,0 +1,3 @@ +class C { + val n = A.callInline +} diff --git a/sbt-test/source-dependencies/inline-rec-change-inline/build.sbt b/sbt-test/source-dependencies/inline-rec-change-inline/build.sbt new file mode 100644 index 000000000000..1036709ccfc1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-inline/build.sbt @@ -0,0 +1,25 @@ +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") +} diff --git a/sbt-test/source-dependencies/inline-rec-change-inline/changes/B1.scala b/sbt-test/source-dependencies/inline-rec-change-inline/changes/B1.scala new file mode 100644 index 000000000000..4a1c47d38572 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-inline/changes/B1.scala @@ -0,0 +1,5 @@ +object B { + + inline def inlinedAny(inline x: String): x.type = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-inline/project/CompileState.scala b/sbt-test/source-dependencies/inline-rec-change-inline/project/CompileState.scala new file mode 100644 index 000000000000..078db9c7bf56 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-inline/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt-test/source-dependencies/inline-rec-change-inline/project/DottyInjectedPlugin.scala b/sbt-test/source-dependencies/inline-rec-change-inline/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..fb946c4b8c61 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-inline/project/DottyInjectedPlugin.scala @@ -0,0 +1,11 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion") + ) +} diff --git a/sbt-test/source-dependencies/inline-rec-change-inline/test b/sbt-test/source-dependencies/inline-rec-change-inline/test new file mode 100644 index 000000000000..c06f4da8cd78 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-inline/test @@ -0,0 +1,9 @@ +> compile +> recordPreviousIterations +# Force recompilation of A because B.inlinedAny, called by A.callInline, has added +# the inline flag to one of its parameters. +$ copy-file changes/B1.scala B.scala +> compile +# 1 to recompile B, then 1 more to recompile A due to B.inlinedAny change, +# then 1 final compilation to recompile C due to A.callInline change +> checkIterations 3 diff --git a/sbt-test/source-dependencies/inline-rec-change-param/A.scala b/sbt-test/source-dependencies/inline-rec-change-param/A.scala new file mode 100644 index 000000000000..d06100a22564 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-param/A.scala @@ -0,0 +1,5 @@ +object A { + + inline def callInline: Any = B.inlinedAny("yyy") + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-param/B.scala b/sbt-test/source-dependencies/inline-rec-change-param/B.scala new file mode 100644 index 000000000000..61e61a620957 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-param/B.scala @@ -0,0 +1,5 @@ +object B { + + inline def inlinedAny(x: String): x.type = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-param/C.scala b/sbt-test/source-dependencies/inline-rec-change-param/C.scala new file mode 100644 index 000000000000..e6511852afa1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-param/C.scala @@ -0,0 +1,3 @@ +class C { + val n = A.callInline +} diff --git a/sbt-test/source-dependencies/inline-rec-change-param/build.sbt b/sbt-test/source-dependencies/inline-rec-change-param/build.sbt new file mode 100644 index 000000000000..1036709ccfc1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-param/build.sbt @@ -0,0 +1,25 @@ +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") +} diff --git a/sbt-test/source-dependencies/inline-rec-change-param/changes/B1.scala b/sbt-test/source-dependencies/inline-rec-change-param/changes/B1.scala new file mode 100644 index 000000000000..5e0e374ed105 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-param/changes/B1.scala @@ -0,0 +1,5 @@ +object B { + + inline def inlinedAny(x: Any): x.type = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-param/project/CompileState.scala b/sbt-test/source-dependencies/inline-rec-change-param/project/CompileState.scala new file mode 100644 index 000000000000..078db9c7bf56 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-param/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt-test/source-dependencies/inline-rec-change-param/project/DottyInjectedPlugin.scala b/sbt-test/source-dependencies/inline-rec-change-param/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..fb946c4b8c61 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-param/project/DottyInjectedPlugin.scala @@ -0,0 +1,11 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion") + ) +} diff --git a/sbt-test/source-dependencies/inline-rec-change-param/test b/sbt-test/source-dependencies/inline-rec-change-param/test new file mode 100644 index 000000000000..22468b12f690 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-param/test @@ -0,0 +1,9 @@ +> compile +> recordPreviousIterations +# Force recompilation of A because B.inlinedAny, called by A.callInline, has changed +# the type of its parameters. +$ copy-file changes/B1.scala B.scala +> compile +# 1 to recompile B, then 1 more to recompile A due to B.inlinedAny change, +# then 1 final compilation to recompile C due to A.callInline change +> checkIterations 3 diff --git a/sbt-test/source-dependencies/inline-rec-change-res-transparent/A.scala b/sbt-test/source-dependencies/inline-rec-change-res-transparent/A.scala new file mode 100644 index 000000000000..d06100a22564 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res-transparent/A.scala @@ -0,0 +1,5 @@ +object A { + + inline def callInline: Any = B.inlinedAny("yyy") + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res-transparent/B.scala b/sbt-test/source-dependencies/inline-rec-change-res-transparent/B.scala new file mode 100644 index 000000000000..8a04e33a0428 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res-transparent/B.scala @@ -0,0 +1,5 @@ +object B { + + transparent inline def inlinedAny(x: String): x.type = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res-transparent/C.scala b/sbt-test/source-dependencies/inline-rec-change-res-transparent/C.scala new file mode 100644 index 000000000000..e6511852afa1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res-transparent/C.scala @@ -0,0 +1,3 @@ +class C { + val n = A.callInline +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res-transparent/build.sbt b/sbt-test/source-dependencies/inline-rec-change-res-transparent/build.sbt new file mode 100644 index 000000000000..1036709ccfc1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res-transparent/build.sbt @@ -0,0 +1,25 @@ +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") +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res-transparent/changes/B1.scala b/sbt-test/source-dependencies/inline-rec-change-res-transparent/changes/B1.scala new file mode 100644 index 000000000000..6cbe84dbb2b8 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res-transparent/changes/B1.scala @@ -0,0 +1,5 @@ +object B { + + transparent inline def inlinedAny(x: String): String = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res-transparent/project/CompileState.scala b/sbt-test/source-dependencies/inline-rec-change-res-transparent/project/CompileState.scala new file mode 100644 index 000000000000..078db9c7bf56 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res-transparent/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res-transparent/project/DottyInjectedPlugin.scala b/sbt-test/source-dependencies/inline-rec-change-res-transparent/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..fb946c4b8c61 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res-transparent/project/DottyInjectedPlugin.scala @@ -0,0 +1,11 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion") + ) +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res-transparent/test b/sbt-test/source-dependencies/inline-rec-change-res-transparent/test new file mode 100644 index 000000000000..7cd958b92dd6 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res-transparent/test @@ -0,0 +1,9 @@ +> compile +> recordPreviousIterations +# Force recompilation of A because B.inlinedAny, called by A.callInline, has changed +# the type of its result, while being an inline method. +$ copy-file changes/B1.scala B.scala +> compile +# 1 to recompile B, then 1 more to recompile A due to B.inlinedAny change, +# then 1 final compilation to recompile C due to A.callInline change +> checkIterations 3 diff --git a/sbt-test/source-dependencies/inline-rec-change-res/A.scala b/sbt-test/source-dependencies/inline-rec-change-res/A.scala new file mode 100644 index 000000000000..d06100a22564 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res/A.scala @@ -0,0 +1,5 @@ +object A { + + inline def callInline: Any = B.inlinedAny("yyy") + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res/B.scala b/sbt-test/source-dependencies/inline-rec-change-res/B.scala new file mode 100644 index 000000000000..61e61a620957 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res/B.scala @@ -0,0 +1,5 @@ +object B { + + inline def inlinedAny(x: String): x.type = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res/C.scala b/sbt-test/source-dependencies/inline-rec-change-res/C.scala new file mode 100644 index 000000000000..e6511852afa1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res/C.scala @@ -0,0 +1,3 @@ +class C { + val n = A.callInline +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res/build.sbt b/sbt-test/source-dependencies/inline-rec-change-res/build.sbt new file mode 100644 index 000000000000..1036709ccfc1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res/build.sbt @@ -0,0 +1,25 @@ +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") +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res/changes/B1.scala b/sbt-test/source-dependencies/inline-rec-change-res/changes/B1.scala new file mode 100644 index 000000000000..eaeef8d57ece --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res/changes/B1.scala @@ -0,0 +1,5 @@ +object B { + + inline def inlinedAny(x: String): String = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res/project/CompileState.scala b/sbt-test/source-dependencies/inline-rec-change-res/project/CompileState.scala new file mode 100644 index 000000000000..078db9c7bf56 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res/project/DottyInjectedPlugin.scala b/sbt-test/source-dependencies/inline-rec-change-res/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..fb946c4b8c61 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res/project/DottyInjectedPlugin.scala @@ -0,0 +1,11 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion") + ) +} diff --git a/sbt-test/source-dependencies/inline-rec-change-res/test b/sbt-test/source-dependencies/inline-rec-change-res/test new file mode 100644 index 000000000000..5972abf58136 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-res/test @@ -0,0 +1,9 @@ +> compile +> recordPreviousIterations +# Force recompilation of A because B.inlinedAny, called by A.callInline, has changed +# the type of its result. +$ copy-file changes/B1.scala B.scala +> compile +# 1 to recompile B, then 1 more to recompile A due to B.inlinedAny change, +# then 1 final compilation to recompile C due to A.callInline change +> checkIterations 3 diff --git a/sbt-test/source-dependencies/inline-rec-change-typaram/A.scala b/sbt-test/source-dependencies/inline-rec-change-typaram/A.scala new file mode 100644 index 000000000000..fadbc929841a --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-typaram/A.scala @@ -0,0 +1,5 @@ +object A { + + inline def callInline: Any = B.inlinedAny(List("yyy")) + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-typaram/B.scala b/sbt-test/source-dependencies/inline-rec-change-typaram/B.scala new file mode 100644 index 000000000000..8501c4c676a0 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-typaram/B.scala @@ -0,0 +1,5 @@ +object B { + + inline def inlinedAny[F[_], T](x: F[T]): x.type = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-typaram/C.scala b/sbt-test/source-dependencies/inline-rec-change-typaram/C.scala new file mode 100644 index 000000000000..e6511852afa1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-typaram/C.scala @@ -0,0 +1,3 @@ +class C { + val n = A.callInline +} diff --git a/sbt-test/source-dependencies/inline-rec-change-typaram/build.sbt b/sbt-test/source-dependencies/inline-rec-change-typaram/build.sbt new file mode 100644 index 000000000000..1036709ccfc1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-typaram/build.sbt @@ -0,0 +1,25 @@ +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") +} diff --git a/sbt-test/source-dependencies/inline-rec-change-typaram/changes/B1.scala b/sbt-test/source-dependencies/inline-rec-change-typaram/changes/B1.scala new file mode 100644 index 000000000000..73e33ccdfd96 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-typaram/changes/B1.scala @@ -0,0 +1,5 @@ +object B { + + inline def inlinedAny[F[X] >: List[X] <: List[X], T <: String](x: F[T]): x.type = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-change-typaram/project/CompileState.scala b/sbt-test/source-dependencies/inline-rec-change-typaram/project/CompileState.scala new file mode 100644 index 000000000000..078db9c7bf56 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-typaram/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt-test/source-dependencies/inline-rec-change-typaram/project/DottyInjectedPlugin.scala b/sbt-test/source-dependencies/inline-rec-change-typaram/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..fb946c4b8c61 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-typaram/project/DottyInjectedPlugin.scala @@ -0,0 +1,11 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion") + ) +} diff --git a/sbt-test/source-dependencies/inline-rec-change-typaram/test b/sbt-test/source-dependencies/inline-rec-change-typaram/test new file mode 100644 index 000000000000..7e18665f3c27 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-change-typaram/test @@ -0,0 +1,9 @@ +> compile +> recordPreviousIterations +# Force recompilation of A because B.inlinedAny, called by A.callInline, has changed +# the type of its type parameters. +$ copy-file changes/B1.scala B.scala +> compile +# 1 to recompile B, then 1 more to recompile A due to B.inlinedAny change, +# then 1 final compilation to recompile C due to A.callInline change +> checkIterations 3 diff --git a/sbt-test/source-dependencies/inline-rec-deep/A.scala b/sbt-test/source-dependencies/inline-rec-deep/A.scala new file mode 100644 index 000000000000..799104b58ba3 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-deep/A.scala @@ -0,0 +1,5 @@ +object A { + + inline def callInline: Any = B.delegated + +} diff --git a/sbt-test/source-dependencies/inline-rec-deep/B.scala b/sbt-test/source-dependencies/inline-rec-deep/B.scala new file mode 100644 index 000000000000..4ed6616db186 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-deep/B.scala @@ -0,0 +1,5 @@ +object B { + + inline def delegated: Any = C.inlinedAny(x = "yyy", y = 23) + +} diff --git a/sbt-test/source-dependencies/inline-rec-deep/C.scala b/sbt-test/source-dependencies/inline-rec-deep/C.scala new file mode 100644 index 000000000000..57ce05b0d2cf --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-deep/C.scala @@ -0,0 +1,5 @@ +object C { + + inline def inlinedAny(x: String, y: Int): x.type = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-deep/D.scala b/sbt-test/source-dependencies/inline-rec-deep/D.scala new file mode 100644 index 000000000000..3f87831200d3 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-deep/D.scala @@ -0,0 +1,3 @@ +class D { + val n = A.callInline +} diff --git a/sbt-test/source-dependencies/inline-rec-deep/build.sbt b/sbt-test/source-dependencies/inline-rec-deep/build.sbt new file mode 100644 index 000000000000..1036709ccfc1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-deep/build.sbt @@ -0,0 +1,25 @@ +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") +} diff --git a/sbt-test/source-dependencies/inline-rec-deep/changes/C1.scala b/sbt-test/source-dependencies/inline-rec-deep/changes/C1.scala new file mode 100644 index 000000000000..4456cc87e252 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-deep/changes/C1.scala @@ -0,0 +1,5 @@ +object C { + + inline def inlinedAny(y: Int, x: String): x.type = x + +} diff --git a/sbt-test/source-dependencies/inline-rec-deep/project/CompileState.scala b/sbt-test/source-dependencies/inline-rec-deep/project/CompileState.scala new file mode 100644 index 000000000000..078db9c7bf56 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-deep/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt-test/source-dependencies/inline-rec-deep/project/DottyInjectedPlugin.scala b/sbt-test/source-dependencies/inline-rec-deep/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..fb946c4b8c61 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-deep/project/DottyInjectedPlugin.scala @@ -0,0 +1,11 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion") + ) +} diff --git a/sbt-test/source-dependencies/inline-rec-deep/test b/sbt-test/source-dependencies/inline-rec-deep/test new file mode 100644 index 000000000000..5381ef195868 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-deep/test @@ -0,0 +1,10 @@ +> compile +> recordPreviousIterations +# Force recompilation of B because C.inlinedAny, called by B.delegated, has changed +# the order of its parameters. +$ copy-file changes/C1.scala C.scala +> compile +# 1 to recompile C, then 1 more to recompile B due to C.inlinedAny change, +# then 1 more to recompile A due to B.delegated change, then 1 final compilation +# to recompile D due to A.callInline change +> checkIterations 4 diff --git a/sbt-test/source-dependencies/inline-rec-mut/A.scala b/sbt-test/source-dependencies/inline-rec-mut/A.scala new file mode 100644 index 000000000000..31e281b8028f --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-mut/A.scala @@ -0,0 +1,8 @@ +object A { + inline def isEven(inline x: Int): Boolean = { + inline if x % 2 == 0 then + true + else + !B.isOdd(x) + } +} diff --git a/sbt-test/source-dependencies/inline-rec-mut/B.scala b/sbt-test/source-dependencies/inline-rec-mut/B.scala new file mode 100644 index 000000000000..d01bf3cc7265 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-mut/B.scala @@ -0,0 +1,8 @@ +object B { + inline def isOdd(inline x: Int): Boolean = { + inline if x % 2 != 0 then + true + else + !A.isEven(x) + } +} diff --git a/sbt-test/source-dependencies/inline-rec-mut/C.scala b/sbt-test/source-dependencies/inline-rec-mut/C.scala new file mode 100644 index 000000000000..d286cb59c060 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-mut/C.scala @@ -0,0 +1,3 @@ +class C { + val n = A.isEven(23) +} diff --git a/sbt-test/source-dependencies/inline-rec-mut/build.sbt b/sbt-test/source-dependencies/inline-rec-mut/build.sbt new file mode 100644 index 000000000000..1036709ccfc1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-mut/build.sbt @@ -0,0 +1,25 @@ +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") +} diff --git a/sbt-test/source-dependencies/inline-rec-mut/changes/B1.scala b/sbt-test/source-dependencies/inline-rec-mut/changes/B1.scala new file mode 100644 index 000000000000..8849df65f516 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-mut/changes/B1.scala @@ -0,0 +1,9 @@ +object B { + inline def isOdd(inline x: Int): Boolean = { + inline if x % 2 == 0 then + val cached = A.isEven(x) + !(cached || cached) + else + true + } +} diff --git a/sbt-test/source-dependencies/inline-rec-mut/project/CompileState.scala b/sbt-test/source-dependencies/inline-rec-mut/project/CompileState.scala new file mode 100644 index 000000000000..078db9c7bf56 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-mut/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt-test/source-dependencies/inline-rec-mut/project/DottyInjectedPlugin.scala b/sbt-test/source-dependencies/inline-rec-mut/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..fb946c4b8c61 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-mut/project/DottyInjectedPlugin.scala @@ -0,0 +1,11 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion") + ) +} diff --git a/sbt-test/source-dependencies/inline-rec-mut/test b/sbt-test/source-dependencies/inline-rec-mut/test new file mode 100644 index 000000000000..0cca3a11d161 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-mut/test @@ -0,0 +1,11 @@ +> compile +> recordPreviousIterations +# Force recompilation of A because B.isOdd, called by A.isEven, +# has changed, this should force C to recompile +$ copy-file changes/B1.scala B.scala +> compile +# 1 to recompile B, then 1 more to recompile: +# - A due to B.isOdd change +# - B due to A.isEven change +# - C due to A.isEven change +> checkIterations 2 diff --git a/sbt-test/source-dependencies/inline-rec-val/A.scala b/sbt-test/source-dependencies/inline-rec-val/A.scala new file mode 100644 index 000000000000..21c4f75fd4f0 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-val/A.scala @@ -0,0 +1,5 @@ +object A { + inline def callInline: Int = inlinedInt + + inline val inlinedInt = 23 +} diff --git a/sbt-test/source-dependencies/inline-rec-val/B.scala b/sbt-test/source-dependencies/inline-rec-val/B.scala new file mode 100644 index 000000000000..eed1cb60271e --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-val/B.scala @@ -0,0 +1,3 @@ +class B { + val n = A.callInline +} diff --git a/sbt-test/source-dependencies/inline-rec-val/build.sbt b/sbt-test/source-dependencies/inline-rec-val/build.sbt new file mode 100644 index 000000000000..1036709ccfc1 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-val/build.sbt @@ -0,0 +1,25 @@ +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") +} diff --git a/sbt-test/source-dependencies/inline-rec-val/changes/A1.scala b/sbt-test/source-dependencies/inline-rec-val/changes/A1.scala new file mode 100644 index 000000000000..2c687006f58f --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-val/changes/A1.scala @@ -0,0 +1,5 @@ +object A { + inline def callInline: Int = inlinedInt + + inline val inlinedInt = 47 +} diff --git a/sbt-test/source-dependencies/inline-rec-val/project/CompileState.scala b/sbt-test/source-dependencies/inline-rec-val/project/CompileState.scala new file mode 100644 index 000000000000..078db9c7bf56 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-val/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt-test/source-dependencies/inline-rec-val/project/DottyInjectedPlugin.scala b/sbt-test/source-dependencies/inline-rec-val/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..fb946c4b8c61 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-val/project/DottyInjectedPlugin.scala @@ -0,0 +1,11 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion") + ) +} diff --git a/sbt-test/source-dependencies/inline-rec-val/test b/sbt-test/source-dependencies/inline-rec-val/test new file mode 100644 index 000000000000..1716f03c6c82 --- /dev/null +++ b/sbt-test/source-dependencies/inline-rec-val/test @@ -0,0 +1,7 @@ +> compile +> recordPreviousIterations +# Force recompilation of B because A.inlinedInt, called by A.callInline, has changed +$ copy-file changes/A1.scala A.scala +> compile +# 1 to recompile A, then 1 more to recompile B due to A.inlinedInt change +> checkIterations 2 diff --git a/sbt-test/source-dependencies/inline-rec/B.scala b/sbt-test/source-dependencies/inline-rec/B.scala index 2b4bd09c6e23..eed1cb60271e 100644 --- a/sbt-test/source-dependencies/inline-rec/B.scala +++ b/sbt-test/source-dependencies/inline-rec/B.scala @@ -1,5 +1,3 @@ class B { - def main(args: Array[String]): Unit = { - assert(A.callInline == C.expected) - } + val n = A.callInline } diff --git a/sbt-test/source-dependencies/inline-rec/C.scala b/sbt-test/source-dependencies/inline-rec/C.scala deleted file mode 100644 index 3b32f0026167..000000000000 --- a/sbt-test/source-dependencies/inline-rec/C.scala +++ /dev/null @@ -1,3 +0,0 @@ -object C { - def expected: Int = 23 -} diff --git a/sbt-test/source-dependencies/inline-rec/changes/C1.scala b/sbt-test/source-dependencies/inline-rec/changes/C1.scala deleted file mode 100644 index 377d5add6ca8..000000000000 --- a/sbt-test/source-dependencies/inline-rec/changes/C1.scala +++ /dev/null @@ -1,3 +0,0 @@ -object C { - def expected: Int = 47 -} diff --git a/sbt-test/source-dependencies/inline-rec/dbg.sbt b/sbt-test/source-dependencies/inline-rec/dbg.sbt deleted file mode 100644 index f9a522c9e610..000000000000 --- a/sbt-test/source-dependencies/inline-rec/dbg.sbt +++ /dev/null @@ -1,3 +0,0 @@ -logLevel := Level.Debug -incOptions in ThisBuild ~= { _.withApiDebug(true) } -incOptions in ThisBuild ~= { _.withRelationsDebug(true) } From 235b07c0e8ee7362f4635d5ec7e0954f163553e5 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Mon, 22 Nov 2021 17:45:59 +0100 Subject: [PATCH 3/8] type ahead symbols from the sourcepath to add defTrees --- .../src/dotty/tools/dotc/sbt/ExtractAPI.scala | 16 +++++++++++----- compiler/src/dotty/tools/dotc/sbt/package.scala | 1 + .../src/dotty/tools/dotc/typer/Implicits.scala | 2 +- library/src/scala/compiletime/package.scala | 1 - 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index 6c65c8f29c9b..16606c6e2068 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -717,11 +717,17 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { val sym = ref.symbol if sym.is(Inline, butNot = Param) && !seenInlines.contains(sym) then seenInlines += sym // dont re-enter hashing this ref - sym.defTree match - case defTree: ValOrDefDef => - h = inlineReferenceHash(sym, defTree.rhs, h) - case _ => - h = err(i"inline method reference `${ref.name}`", ref.name, ref, h) + if sym.is(Method) then + Inliner.bodyToInline(sym) match // force typechecking of body if from source + case EmptyTree => + h = err("inline method reference", ref, p, h) + case rhs => + h = inlineReferenceHash(sym, rhs, h) + else + // inline value - its rhs should match its type + // no extra info is gained from hashing the rhs + h = MurmurHash3.mix(h, InlineValHash) + h = inlineReferenceHash(sym, EmptyTree, h) case _ => // FIXME: If `p` is a tree we should probably take its type into account diff --git a/compiler/src/dotty/tools/dotc/sbt/package.scala b/compiler/src/dotty/tools/dotc/sbt/package.scala index 99a6b97bddef..56503608a986 100644 --- a/compiler/src/dotty/tools/dotc/sbt/package.scala +++ b/compiler/src/dotty/tools/dotc/sbt/package.scala @@ -9,6 +9,7 @@ inline val TermNameHash = 1987 // 300th prime inline val TypeNameHash = 1993 // 301st prime inline val EmptyParamHash = 1997 // 302nd prime inline val InlineParamHash = 1999 // 303rd prime +inline val InlineValHash = 2003 // 304th prime extension (sym: Symbol) diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index cc058cfc2c6e..2d6d1754fbea 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -980,7 +980,7 @@ trait Implicits: trace(s"search implicit ${pt.show}, arg = ${argument.show}: ${argument.tpe.show}", implicits, show = true) { record("inferImplicit") assert(ctx.phase.allowsImplicitSearch, - if (argument.isEmpty) i"missing implicit parameter of type $pt after typer" + if (argument.isEmpty) i"missing implicit parameter of type $pt after typer at phase ${ctx.phase.phaseName}" else i"type error: ${argument.tpe} does not conform to $pt${err.whyNoMatchStr(argument.tpe, pt)}") if pt.unusableForInference diff --git a/library/src/scala/compiletime/package.scala b/library/src/scala/compiletime/package.scala index d65fd235e0c3..754ce4d0b9d1 100644 --- a/library/src/scala/compiletime/package.scala +++ b/library/src/scala/compiletime/package.scala @@ -188,4 +188,3 @@ def byName[T](x: => T): T = x */ extension [T](x: T) transparent inline def asMatchable: x.type & Matchable = x.asInstanceOf[x.type & Matchable] - From 1c560903671b3c9c630e551d1ef3386a154c3100 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Wed, 15 Dec 2021 12:51:27 +0100 Subject: [PATCH 4/8] test extractAPI of inline from sourcepath --- project/scripts/bootstrapCmdTests | 18 ++++++++++++++++-- .../sourcepath-with-inline-api-hash/build.sbt | 17 +++++++++++++++++ .../changes/zz.new.scala | 7 +++++++ .../changes/zz.original.scala | 6 ++++++ .../project/build.properties | 1 + .../src/main/scala/a/Bar.scala | 5 +++++ 6 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/build.sbt create mode 100644 tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/changes/zz.new.scala create mode 100644 tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/changes/zz.original.scala create mode 100644 tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/project/build.properties create mode 100644 tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/src/main/scala/a/Bar.scala diff --git a/project/scripts/bootstrapCmdTests b/project/scripts/bootstrapCmdTests index d1ec6138acb3..472426b23b20 100755 --- a/project/scripts/bootstrapCmdTests +++ b/project/scripts/bootstrapCmdTests @@ -96,8 +96,8 @@ IFS=':=' read -ra versionProps < "$cwd/dist/target/pack/VERSION" # temporarily s [ -n ${versionProps[2]} ] || die "Expected non-empty 'version' property in $cwd/dist/target/pack/VERSION" scala_version=${versionProps[2]} -echo "testing -sourcepath with inlining" -# Here we will test that an inline method symbol loaded from the sourcepath (-sourcepath compiler option) +echo "testing -sourcepath with incremental compile: inlining changed inline def into a def" +# Here we will test that a changed inline method symbol loaded from the sourcepath (-sourcepath compiler option) # will have its `defTree` correctly set when its method body is required for inlining. # So far I have not found a way to replicate issue https://github.com/lampepfl/dotty/issues/13994 # with sbt scripted tests, if a way is found, move this test there. @@ -107,3 +107,17 @@ sbt_test_command="++${scala_version}!;clean;prepareSources;compile;copyChanges;c rm -rf "$cwd/tests/cmdTest-sbt-tests/sourcepath-with-inline/target" rm -rf "$cwd/tests/cmdTest-sbt-tests/sourcepath-with-inline/project/target" rm -f "$cwd/tests/cmdTest-sbt-tests/sourcepath-with-inline/src/main/scala/a/zz.scala" + +echo "testing -sourcepath with incremental compile: hashing reference to changed inline def from an inline def" +# Here we will test that a changed inline method symbol loaded from the sourcepath (-sourcepath compiler option) +# will have its `defTree` correctly set when its method body is hashed by extractAPI, when referenced from another +# inline method. +# So far I have not found a way to replicate https://github.com/lampepfl/dotty/pull/12931#discussion_r753212124 +# with sbt scripted tests, if a way is found, move this test there. +cwd=$(pwd) +sbt_test_dir="$cwd/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash" +sbt_test_command="++${scala_version}!;clean;prepareSources;compile;copyChanges;compile" +(cd "$sbt_test_dir" && "$SBT" "$sbt_test_command") +rm -rf "$sbt_test_dir/target" +rm -rf "$sbt_test_dir/project/target" +rm -f "$sbt_test_dir/src/main/scala/a/zz.scala" diff --git a/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/build.sbt b/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/build.sbt new file mode 100644 index 000000000000..4bff160ff55a --- /dev/null +++ b/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/build.sbt @@ -0,0 +1,17 @@ +import java.util.Properties + +val prepareSources = taskKey[Unit]("Copy changes to the src directory") +val copyChanges = taskKey[Unit]("Copy changes to the src directory") + +val srcDir = settingKey[File]("The directory to copy changes to") +val changesDir = settingKey[File]("The directory to copy changes from") + +srcDir := (ThisBuild / baseDirectory).value / "src" / "main" / "scala" +changesDir := (ThisBuild / baseDirectory).value / "changes" + +prepareSources := IO.copyFile(changesDir.value / "zz.original.scala", srcDir.value / "a" / "zz.scala") +copyChanges := IO.copyFile(changesDir.value / "zz.new.scala", srcDir.value / "a" / "zz.scala") + +(Compile / scalacOptions) ++= Seq( + "-sourcepath", (Compile / sourceDirectories).value.map(_.getAbsolutePath).distinct.mkString(java.io.File.pathSeparator), +) diff --git a/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/changes/zz.new.scala b/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/changes/zz.new.scala new file mode 100644 index 000000000000..fbf5cf7fb5e0 --- /dev/null +++ b/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/changes/zz.new.scala @@ -0,0 +1,7 @@ +package a + +object Foo: // note that `Foo` is defined in `zz.scala` + class Local + inline def foo(using Local): Nothing = + ??? + ??? diff --git a/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/changes/zz.original.scala b/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/changes/zz.original.scala new file mode 100644 index 000000000000..17a7488ccb1a --- /dev/null +++ b/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/changes/zz.original.scala @@ -0,0 +1,6 @@ +package a + +object Foo: // note that `Foo` is defined in `zz.scala` + class Local + inline def foo(using Local): Nothing = + ??? diff --git a/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/project/build.properties b/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/project/build.properties new file mode 100644 index 000000000000..10fd9eee04ac --- /dev/null +++ b/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.5.5 diff --git a/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/src/main/scala/a/Bar.scala b/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/src/main/scala/a/Bar.scala new file mode 100644 index 000000000000..4d4b7eebe09e --- /dev/null +++ b/tests/cmdTest-sbt-tests/sourcepath-with-inline-api-hash/src/main/scala/a/Bar.scala @@ -0,0 +1,5 @@ +package a + +object Bar: + given Foo.Local() + inline def bar = Foo.foo // `Bar.bar` is inline, it will hash the body of `Foo.foo` From 7bde05facb3e6588fd8458092a59559c446dc99a Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Wed, 15 Dec 2021 19:01:40 +0100 Subject: [PATCH 5/8] address comments move the responsibility of closing the inline loop to apiAnnotations, which hashes the inline body annotation. Also add a test for changing second parameter list of extension methods. --- .../src/dotty/tools/dotc/sbt/ExtractAPI.scala | 232 +++++++++++------- .../src/dotty/tools/dotc/sbt/package.scala | 4 +- .../extension-change-second-tparams/A.scala | 7 + .../extension-change-second-tparams/B.scala | 6 + .../extension-change-second-tparams/build.sbt | 25 ++ .../changes/A1.scala | 7 + .../project/CompileState.scala | 4 + .../project/DottyInjectedPlugin.scala | 11 + .../extension-change-second-tparams/test | 7 + 9 files changed, 217 insertions(+), 86 deletions(-) create mode 100644 sbt-test/source-dependencies/extension-change-second-tparams/A.scala create mode 100644 sbt-test/source-dependencies/extension-change-second-tparams/B.scala create mode 100644 sbt-test/source-dependencies/extension-change-second-tparams/build.sbt create mode 100644 sbt-test/source-dependencies/extension-change-second-tparams/changes/A1.scala create mode 100644 sbt-test/source-dependencies/extension-change-second-tparams/project/CompileState.scala create mode 100644 sbt-test/source-dependencies/extension-change-second-tparams/project/DottyInjectedPlugin.scala create mode 100644 sbt-test/source-dependencies/extension-change-second-tparams/test diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index 16606c6e2068..de0fbb8de807 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -24,6 +24,7 @@ import java.io.PrintWriter import xsbti.api.DefinitionType import scala.collection.mutable +import scala.util.hashing.MurmurHash3 /** This phase sends a representation of the API of classes to sbt via callbacks. * @@ -143,6 +144,12 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { */ private val refinedTypeCache = new mutable.HashMap[(api.Type, api.Definition), api.Structure] + /** This cache is necessary to avoid infinite loops when hashing the body of inline definitions. + * Its keys represent the root inline definitions, and its values are seen inline references within + * the rhs of the key. If a symbol is present in the value set, then do not hash its signature or inline body. + */ + private val seenInlineCache = mutable.HashMap.empty[Symbol, mutable.HashSet[Symbol]] + private val allNonLocalClassesInSrc = new mutable.HashSet[xsbti.api.ClassLike] private val _mainClasses = new mutable.HashSet[String] @@ -219,7 +226,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { val structure = apiClassStructure(sym) val acc = apiAccess(sym) val modifiers = apiModifiers(sym) - val anns = apiAnnotations(sym).toArray + val anns = apiAnnotations(sym, inlineOrigin = NoSymbol).toArray val topLevel = sym.isTopLevelClass val childrenOfSealedClass = sym.sealedDescendants.sorted(classFirstSort).map(c => if (c.isClass) @@ -320,54 +327,97 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { } } - def apiDefinitions(defs: List[Symbol]): List[api.ClassDefinition] = { - defs.sorted(classFirstSort).map(apiDefinition) - } + def apiDefinitions(defs: List[Symbol]): List[api.ClassDefinition] = + defs.sorted(classFirstSort).map(apiDefinition(_, inlineOrigin = NoSymbol)) - def apiDefinition(sym: Symbol): api.ClassDefinition = { + /** `inlineOrigin` denotes an optional inline method that we are + * currently hashing the body of. If it exists, include extra information + * that is missing after erasure + */ + def apiDefinition(sym: Symbol, inlineOrigin: Symbol): api.ClassDefinition = { if (sym.isClass) { apiClass(sym.asClass) } else if (sym.isType) { apiTypeMember(sym.asType) } else if (sym.is(Mutable, butNot = Accessor)) { api.Var.of(sym.name.toString, apiAccess(sym), apiModifiers(sym), - apiAnnotations(sym).toArray, apiType(sym.info)) + apiAnnotations(sym, inlineOrigin).toArray, apiType(sym.info)) } else if (sym.isStableMember && !sym.isRealMethod) { api.Val.of(sym.name.toString, apiAccess(sym), apiModifiers(sym), - apiAnnotations(sym).toArray, apiType(sym.info)) + apiAnnotations(sym, inlineOrigin).toArray, apiType(sym.info)) } else { - apiDef(sym.asTerm) + apiDef(sym.asTerm, inlineOrigin) } } - def apiDef(sym: TermSymbol): api.Def = { + /** `inlineOrigin` denotes an optional inline method that we are + * currently hashing the body of. If it exists, include extra information + * that is missing after erasure + */ + def apiDef(sym: TermSymbol, inlineOrigin: Symbol): api.Def = { + + val inlineExtras = new mutable.ListBuffer[Int => Int] + + def mixInlineParam(p: Symbol): Unit = + if inlineOrigin.exists && p.is(Inline) then + inlineExtras += hashInlineParam(p) + + def inlineExtrasAnnot: Option[api.Annotation] = + Option.when(inlineOrigin.exists && inlineExtras.nonEmpty) { + marker(s"${hashList(inlineExtras.toList)("inlineExtras".hashCode)}") + } + + def tparamList(pt: TypeLambda): List[api.TypeParameter] = + pt.paramNames.lazyZip(pt.paramInfos).map((pname, pbounds) => + apiTypeParameter(pname.toString, 0, pbounds.lo, pbounds.hi) + ) + + def paramList(mt: MethodType, params: List[Symbol]): api.ParameterList = + val apiParams = params.lazyZip(mt.paramInfos).map((param, ptype) => + mixInlineParam(param) + api.MethodParameter.of( + param.name.toString, apiType(ptype), param.is(HasDefault), api.ParameterModifier.Plain)) + api.ParameterList.of(apiParams.toArray, mt.isImplicitMethod) + def paramLists(t: Type, paramss: List[List[Symbol]]): List[api.ParameterList] = t match { case pt: TypeLambda => paramLists(pt.resultType, paramss.drop(1)) case mt @ MethodTpe(pnames, ptypes, restpe) => assert(paramss.nonEmpty && paramss.head.hasSameLengthAs(pnames), i"mismatch for $sym, ${sym.info}, ${sym.paramSymss}") - val apiParams = paramss.head.lazyZip(ptypes).map((param, ptype) => - api.MethodParameter.of(param.name.toString, apiType(ptype), - param.is(HasDefault), api.ParameterModifier.Plain)) - api.ParameterList.of(apiParams.toArray, mt.isImplicitMethod) - :: paramLists(restpe, paramss.tail) + paramList(mt, paramss.head) :: paramLists(restpe, paramss.tail) case _ => Nil } - val tparams = sym.info match { + /** returns list of pairs of 1: the position in all parameter lists, and 2: a type parameter list */ + def tparamLists(t: Type, index: Int): List[(Int, List[api.TypeParameter])] = t match case pt: TypeLambda => - pt.paramNames.lazyZip(pt.paramInfos).map((pname, pbounds) => - apiTypeParameter(pname.toString, 0, pbounds.lo, pbounds.hi)) + (index, tparamList(pt)) :: tparamLists(pt.resultType, index + 1) + case mt: MethodType => + tparamLists(mt.resultType, index + 1) case _ => Nil - } + + val (tparams, tparamsExtras) = sym.info match + case pt: TypeLambda => + (tparamList(pt), tparamLists(pt.resultType, index = 1)) + case mt: MethodType => + (Nil, tparamLists(mt.resultType, index = 1)) + case _ => + (Nil, Nil) + val vparamss = paramLists(sym.info, sym.paramSymss) val retTp = sym.info.finalResultType.widenExpr + val tparamsExtraAnnot = Option.when(tparamsExtras.nonEmpty) { + marker(s"${hashTparamsExtras(tparamsExtras)("tparamsExtra".hashCode)}") + } + + val annotations = inlineExtrasAnnot ++: tparamsExtraAnnot ++: apiAnnotations(sym, inlineOrigin) + api.Def.of(sym.zincMangledName.toString, apiAccess(sym), apiModifiers(sym), - apiAnnotations(sym).toArray, tparams.toArray, vparamss.toArray, apiType(retTp)) + annotations.toArray, tparams.toArray, vparamss.toArray, apiType(retTp)) } def apiTypeMember(sym: TypeSymbol): api.TypeMember = { @@ -375,7 +425,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { val name = sym.name.toString val access = apiAccess(sym) val modifiers = apiModifiers(sym) - val as = apiAnnotations(sym) + val as = apiAnnotations(sym, inlineOrigin = NoSymbol) val tpe = sym.info if (sym.isAliasType) @@ -585,10 +635,13 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { sym.isOneOf(GivenOrImplicit), sym.is(Lazy), sym.is(Macro), sym.isSuperAccessor) } - def apiAnnotations(s: Symbol): List[api.Annotation] = { + /** `inlineOrigin` denotes an optional inline method that we are + * currently hashing the body of. + */ + def apiAnnotations(s: Symbol, inlineOrigin: Symbol): List[api.Annotation] = { val annots = new mutable.ListBuffer[api.Annotation] val inlineBody = Inliner.bodyToInline(s) - if (!inlineBody.isEmpty) { + if !inlineBody.isEmpty then // If the body of an inline def changes, all the reverse dependencies of // this method need to be recompiled. sbt has no way of tracking method // bodies, so we include the hash of the body of the method as part of the @@ -599,9 +652,18 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { // an inline def in every class that extends its owner. To avoid this we // could store the hash as an annotation when pickling an inline def // and retrieve it here instead of computing it on the fly. - val inlineBodyHash = treeHash(inlineBody, inlineSym = s) - annots += marker(inlineBodyHash.toString) - } + val root = + if inlineOrigin.exists then + inlineOrigin + else + assert(!seenInlineCache.contains(s)) + seenInlineCache.put(s, mutable.HashSet.empty) + s + if !seenInlineCache(root).contains(s) then + seenInlineCache(root) += s + val inlineBodyHash = treeHash(inlineBody, inlineOrigin = root) + annots += marker(inlineBodyHash.toString) + end if // In the Scala2 ExtractAPI phase we only extract annotations that extend // StaticAnnotation, but in Dotty we currently pickle all annotations so we @@ -619,16 +681,15 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { /** Produce a hash for a tree that is as stable as possible: * it should stay the same across compiler runs, compiler instances, * JVMs, etc. + * + * `inlineOrigin` denotes an optional inline method that we are hashing the body of, where `tree` could be + * its body, or the body of another method referenced in a call chain leading to `inlineOrigin`. + * + * If `inlineOrigin` is NoSymbol, then tree is the tree of an annotation. */ - def treeHash(tree: Tree, inlineSym: Symbol): Int = - import scala.util.hashing.MurmurHash3 + def treeHash(tree: Tree, inlineOrigin: Symbol): Int = import core.Constants.* - val seenInlines = mutable.HashSet.empty[Symbol] - - if inlineSym ne NoSymbol then - seenInlines += inlineSym // do not hash twice a recursive def - def nameHash(n: Name, initHash: Int): Int = val h = if n.isTermName then @@ -651,7 +712,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { tp match case ConstantType(c) => h = constantHash(c, h) - case TypeBounds(lo, hi) => + case TypeBounds(lo, hi) => // TODO when does this happen? h = MurmurHash3.mix(h, apiType(lo).hashCode) h = MurmurHash3.mix(h, apiType(hi).hashCode) case tp => @@ -671,38 +732,8 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { h end constantHash - /**An inline method that calls another inline method will eventually inline the call - * at a non-inline callsite, in this case if the implementation of the nested call - * changes, then the callsite will have a different API, we should hash the definition - */ - def inlineReferenceHash(ref: Symbol, rhs: Tree, initHash: Int): Int = - var h = initHash - - def paramssHash(paramss: List[List[Symbol]], initHash: Int): Int = paramss match - case Nil :: paramss1 => - paramssHash(paramss1, MurmurHash3.mix(initHash, EmptyParamHash)) - case params :: paramss1 => - var h = initHash - val paramsIt = params.iterator - while paramsIt.hasNext do - val param = paramsIt.next - h = nameHash(param.name, h) - h = typeHash(param.info, h) - if param.is(Inline) then - h = MurmurHash3.mix(h, InlineParamHash) // inline would change the generated code - paramssHash(paramss1, h) - case Nil => - initHash - end paramssHash - - h = paramssHash(ref.paramSymss, h) - h = typeHash(ref.info.finalResultType, h) - positionedHash(rhs, h) - end inlineReferenceHash - - def err(what: String, elem: Any, pos: Positioned, initHash: Int): Int = + def cannotHash(what: String, elem: Any, pos: Positioned): Unit = internalError(i"Don't know how to produce a stable hash for $what", pos.sourcePos) - MurmurHash3.mix(initHash, elem.toString.hashCode) def positionedHash(p: ast.Positioned, initHash: Int): Int = var h = initHash @@ -712,23 +743,16 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { p.forceIfLazy case _ => - p match - case ref: RefTree @unchecked => - val sym = ref.symbol - if sym.is(Inline, butNot = Param) && !seenInlines.contains(sym) then - seenInlines += sym // dont re-enter hashing this ref - if sym.is(Method) then - Inliner.bodyToInline(sym) match // force typechecking of body if from source - case EmptyTree => - h = err("inline method reference", ref, p, h) - case rhs => - h = inlineReferenceHash(sym, rhs, h) - else - // inline value - its rhs should match its type - // no extra info is gained from hashing the rhs - h = MurmurHash3.mix(h, InlineValHash) - h = inlineReferenceHash(sym, EmptyTree, h) - case _ => + if inlineOrigin.exists then + p match + case ref: RefTree @unchecked => + val sym = ref.symbol + if sym.is(Inline, butNot = Param) && !seenInlineCache(inlineOrigin).contains(sym) then + // An inline method that calls another inline method will eventually inline the call + // at a non-inline callsite, in this case if the implementation of the nested call + // changes, then the callsite will have a different API, we should hash the definition + h = MurmurHash3.mix(h, apiDefinition(sym, inlineOrigin).hashCode) + case _ => // FIXME: If `p` is a tree we should probably take its type into account // when hashing it, but producing a stable hash for a type is not trivial @@ -754,7 +778,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { case n: Name => h = nameHash(n, h) case elem => - h = err(i"`$elem` of unknown class ${elem.getClass}", elem, tree, h) + cannotHash(what = i"`$elem` of unknown class ${elem.getClass}", elem, tree) h end iteratorHash @@ -763,6 +787,48 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { MurmurHash3.finalizeHash(h, 0) end treeHash + /** Hash secondary type parameters in separate marker annotation. + * We hash them separately because the position of type parameters is important. + */ + private def hashTparamsExtras(tparamsExtras: List[(Int, List[api.TypeParameter])])(initHash: Int): Int = + + def mixTparams(tparams: List[api.TypeParameter])(initHash: Int) = + var h = initHash + var elems = tparams + while elems.nonEmpty do + h = MurmurHash3.mix(h, elems.head.hashCode) + elems = elems.tail + h + + def mixIndexAndTparams(index: Int, tparams: List[api.TypeParameter])(initHash: Int) = + mixTparams(tparams)(MurmurHash3.mix(initHash, index)) + + var h = initHash + var extras = tparamsExtras + var len = 0 + while extras.nonEmpty do + h = mixIndexAndTparams(index = extras.head(0), tparams = extras.head(1))(h) + extras = extras.tail + len += 1 + MurmurHash3.finalizeHash(h, len) + end hashTparamsExtras + + private def hashList(extraHashes: List[Int => Int])(initHash: Int): Int = + var h = initHash + var fs = extraHashes + var len = 0 + while fs.nonEmpty do + h = fs.head(h) + fs = fs.tail + len += 1 + MurmurHash3.finalizeHash(h, len) + + /** Mix in the name hash also because otherwise switching which + * parameter is inline will not affect the hash. + */ + private def hashInlineParam(p: Symbol)(h: Int) = + MurmurHash3.mix(p.name.toString.hashCode, MurmurHash3.mix(h, InlineParamHash)) + def apiAnnotation(annot: Annotation): api.Annotation = { // Like with inline defs, the whole body of the annotation and not just its // type is part of its API so we need to store its hash, but Zinc wants us @@ -773,6 +839,6 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { // annotated @org.junit.Test). api.Annotation.of( apiType(annot.tree.tpe), // Used by sbt to find tests to run - Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree, inlineSym = NoSymbol).toString))) + Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree, inlineOrigin = NoSymbol).toString))) } } diff --git a/compiler/src/dotty/tools/dotc/sbt/package.scala b/compiler/src/dotty/tools/dotc/sbt/package.scala index 56503608a986..379a2e45ce40 100644 --- a/compiler/src/dotty/tools/dotc/sbt/package.scala +++ b/compiler/src/dotty/tools/dotc/sbt/package.scala @@ -7,9 +7,7 @@ import dotty.tools.dotc.core.Names.Name inline val TermNameHash = 1987 // 300th prime inline val TypeNameHash = 1993 // 301st prime -inline val EmptyParamHash = 1997 // 302nd prime -inline val InlineParamHash = 1999 // 303rd prime -inline val InlineValHash = 2003 // 304th prime +inline val InlineParamHash = 1997 // 302nd prime extension (sym: Symbol) diff --git a/sbt-test/source-dependencies/extension-change-second-tparams/A.scala b/sbt-test/source-dependencies/extension-change-second-tparams/A.scala new file mode 100644 index 000000000000..9726ab97fa95 --- /dev/null +++ b/sbt-test/source-dependencies/extension-change-second-tparams/A.scala @@ -0,0 +1,7 @@ +object A { + + class Box[T](val value: T) + + extension (box: Box[Int]) def map[I <: Int](f: Int => I): Box[I] = new Box(f(box.value)) + +} diff --git a/sbt-test/source-dependencies/extension-change-second-tparams/B.scala b/sbt-test/source-dependencies/extension-change-second-tparams/B.scala new file mode 100644 index 000000000000..9faedc0666d3 --- /dev/null +++ b/sbt-test/source-dependencies/extension-change-second-tparams/B.scala @@ -0,0 +1,6 @@ +import A.Box +import A.map + +class B { + val n = Box(5).map(x => x * 3) +} diff --git a/sbt-test/source-dependencies/extension-change-second-tparams/build.sbt b/sbt-test/source-dependencies/extension-change-second-tparams/build.sbt new file mode 100644 index 000000000000..1036709ccfc1 --- /dev/null +++ b/sbt-test/source-dependencies/extension-change-second-tparams/build.sbt @@ -0,0 +1,25 @@ +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") +} diff --git a/sbt-test/source-dependencies/extension-change-second-tparams/changes/A1.scala b/sbt-test/source-dependencies/extension-change-second-tparams/changes/A1.scala new file mode 100644 index 000000000000..505d037ceb6b --- /dev/null +++ b/sbt-test/source-dependencies/extension-change-second-tparams/changes/A1.scala @@ -0,0 +1,7 @@ +object A { + + class Box[T](val value: T) + + extension (box: Box[Int]) def map[I](f: Int => I): Box[I] = new Box(f(box.value)) + +} diff --git a/sbt-test/source-dependencies/extension-change-second-tparams/project/CompileState.scala b/sbt-test/source-dependencies/extension-change-second-tparams/project/CompileState.scala new file mode 100644 index 000000000000..078db9c7bf56 --- /dev/null +++ b/sbt-test/source-dependencies/extension-change-second-tparams/project/CompileState.scala @@ -0,0 +1,4 @@ +// This is necessary because tests are run in batch mode +object CompileState { + @volatile var previousIterations: Int = -1 +} diff --git a/sbt-test/source-dependencies/extension-change-second-tparams/project/DottyInjectedPlugin.scala b/sbt-test/source-dependencies/extension-change-second-tparams/project/DottyInjectedPlugin.scala new file mode 100644 index 000000000000..fb946c4b8c61 --- /dev/null +++ b/sbt-test/source-dependencies/extension-change-second-tparams/project/DottyInjectedPlugin.scala @@ -0,0 +1,11 @@ +import sbt._ +import Keys._ + +object DottyInjectedPlugin extends AutoPlugin { + override def requires = plugins.JvmPlugin + override def trigger = allRequirements + + override val projectSettings = Seq( + scalaVersion := sys.props("plugin.scalaVersion") + ) +} diff --git a/sbt-test/source-dependencies/extension-change-second-tparams/test b/sbt-test/source-dependencies/extension-change-second-tparams/test new file mode 100644 index 000000000000..9b96674ac362 --- /dev/null +++ b/sbt-test/source-dependencies/extension-change-second-tparams/test @@ -0,0 +1,7 @@ +> compile +> recordPreviousIterations +# Force recompilation of B because A.map, called by B.n, has changed +$ copy-file changes/A1.scala A.scala +> compile +# 1 to recompile A, then 1 more to recompile B due to A.map change +> checkIterations 2 From 273239deea184728202af8478fe0567d03b72110 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Thu, 16 Dec 2021 14:47:24 +0100 Subject: [PATCH 6/8] cache inherited inline body caches --- .../src/dotty/tools/dotc/sbt/ExtractAPI.scala | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index de0fbb8de807..8cbe7d2a1d7c 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -149,6 +149,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { * the rhs of the key. If a symbol is present in the value set, then do not hash its signature or inline body. */ private val seenInlineCache = mutable.HashMap.empty[Symbol, mutable.HashSet[Symbol]] + private val inlineBodyCache = mutable.HashMap.empty[Symbol, Int] private val allNonLocalClassesInSrc = new mutable.HashSet[xsbti.api.ClassLike] private val _mainClasses = new mutable.HashSet[String] @@ -652,17 +653,27 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { // an inline def in every class that extends its owner. To avoid this we // could store the hash as an annotation when pickling an inline def // and retrieve it here instead of computing it on the fly. - val root = - if inlineOrigin.exists then - inlineOrigin - else - assert(!seenInlineCache.contains(s)) - seenInlineCache.put(s, mutable.HashSet.empty) - s - if !seenInlineCache(root).contains(s) then - seenInlineCache(root) += s - val inlineBodyHash = treeHash(inlineBody, inlineOrigin = root) + + def registerInlineHash(inlineBodyHash: Int): Unit = annots += marker(inlineBodyHash.toString) + + def nestedHash(root: Symbol): Unit = + if !seenInlineCache(root).contains(s) then + seenInlineCache(root) += s + registerInlineHash(treeHash(inlineBody, inlineOrigin = root)) + + def originHash(root: Symbol): Unit = + def computeHash(): Int = + assert(!seenInlineCache.contains(root)) + seenInlineCache.put(root, mutable.HashSet(root)) + val res = treeHash(inlineBody, inlineOrigin = root) + seenInlineCache.remove(root) + res + registerInlineHash(inlineBodyCache.getOrElseUpdate(root, computeHash())) + + if inlineOrigin.exists then nestedHash(root = inlineOrigin) + else originHash(root = s) + end if // In the Scala2 ExtractAPI phase we only extract annotations that extend From 33ebf01d3a611512d6514073c138047850468c60 Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Tue, 21 Dec 2021 16:45:13 +0100 Subject: [PATCH 7/8] address comments --- .../src/dotty/tools/dotc/sbt/ExtractAPI.scala | 90 +++++++------------ 1 file changed, 32 insertions(+), 58 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index 8cbe7d2a1d7c..5bbc52e09a82 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -25,6 +25,7 @@ import xsbti.api.DefinitionType import scala.collection.mutable import scala.util.hashing.MurmurHash3 +import scala.util.chaining.* /** This phase sends a representation of the API of classes to sbt via callbacks. * @@ -141,14 +142,17 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { /** This cache is necessary to avoid unstable name hashing when `typeCache` is present, * see the comment in the `RefinedType` case in `computeType` * The cache key is (api of RefinedType#parent, api of RefinedType#refinedInfo). - */ + */ private val refinedTypeCache = new mutable.HashMap[(api.Type, api.Definition), api.Structure] - /** This cache is necessary to avoid infinite loops when hashing the body of inline definitions. - * Its keys represent the root inline definitions, and its values are seen inline references within - * the rhs of the key. If a symbol is present in the value set, then do not hash its signature or inline body. + /** This cache is necessary to avoid infinite loops when hashing an inline "Body" annotation. + * Its values are transitively seen inline references within a call chain starting from a single "origin" inline + * definition. Avoid hashing an inline "Body" annotation if its associated definition is already in the cache. + * Precondition: the cache is empty whenever we hash a new "origin" inline "Body" annotation. */ - private val seenInlineCache = mutable.HashMap.empty[Symbol, mutable.HashSet[Symbol]] + private val seenInlineCache = mutable.HashSet.empty[Symbol] + + /** This cache is optional, it avoids recomputing hashes of inline "Body" annotations. */ private val inlineBodyCache = mutable.HashMap.empty[Symbol, Int] private val allNonLocalClassesInSrc = new mutable.HashSet[xsbti.api.ClassLike] @@ -357,15 +361,18 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { */ def apiDef(sym: TermSymbol, inlineOrigin: Symbol): api.Def = { - val inlineExtras = new mutable.ListBuffer[Int => Int] + var seenInlineExtras = false + var inlineExtras = 41 def mixInlineParam(p: Symbol): Unit = if inlineOrigin.exists && p.is(Inline) then - inlineExtras += hashInlineParam(p) + seenInlineExtras = true + inlineExtras = hashInlineParam(p, inlineExtras) def inlineExtrasAnnot: Option[api.Annotation] = - Option.when(inlineOrigin.exists && inlineExtras.nonEmpty) { - marker(s"${hashList(inlineExtras.toList)("inlineExtras".hashCode)}") + val h = inlineExtras + Option.when(seenInlineExtras) { + marker(s"${MurmurHash3.finalizeHash(h, "inlineExtras".hashCode)}") } def tparamList(pt: TypeLambda): List[api.TypeParameter] = @@ -654,25 +661,15 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { // could store the hash as an annotation when pickling an inline def // and retrieve it here instead of computing it on the fly. - def registerInlineHash(inlineBodyHash: Int): Unit = - annots += marker(inlineBodyHash.toString) - - def nestedHash(root: Symbol): Unit = - if !seenInlineCache(root).contains(s) then - seenInlineCache(root) += s - registerInlineHash(treeHash(inlineBody, inlineOrigin = root)) + def hash[U](inlineOrigin: Symbol): Int = + assert(seenInlineCache.add(s)) // will fail if already seen, guarded by treeHash + treeHash(inlineBody, inlineOrigin) - def originHash(root: Symbol): Unit = - def computeHash(): Int = - assert(!seenInlineCache.contains(root)) - seenInlineCache.put(root, mutable.HashSet(root)) - val res = treeHash(inlineBody, inlineOrigin = root) - seenInlineCache.remove(root) - res - registerInlineHash(inlineBodyCache.getOrElseUpdate(root, computeHash())) + val inlineHash = + if inlineOrigin.exists then hash(inlineOrigin) + else inlineBodyCache.getOrElseUpdate(s, hash(inlineOrigin = s).tap(_ => seenInlineCache.clear())) - if inlineOrigin.exists then nestedHash(root = inlineOrigin) - else originHash(root = s) + annots += marker(inlineHash.toString) end if @@ -712,32 +709,19 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { MurmurHash3.mix(h, n.toString.hashCode) end nameHash - def typeHash(tp: Type, initHash: Int): Int = - // Go through `apiType` to get a value with a stable hash, it'd - // be better to use Murmur here too instead of relying on - // `hashCode`, but that would essentially mean duplicating - // https://github.com/sbt/zinc/blob/develop/internal/zinc-apiinfo/src/main/scala/xsbt/api/HashAPI.scala - // and at that point we might as well do type hashing on our own - // representation. - var h = initHash - tp match - case ConstantType(c) => - h = constantHash(c, h) - case TypeBounds(lo, hi) => // TODO when does this happen? - h = MurmurHash3.mix(h, apiType(lo).hashCode) - h = MurmurHash3.mix(h, apiType(hi).hashCode) - case tp => - h = MurmurHash3.mix(h, apiType(tp).hashCode) - h - end typeHash - def constantHash(c: Constant, initHash: Int): Int = var h = MurmurHash3.mix(initHash, c.tag) c.tag match case NullTag => // No value to hash, the tag is enough. case ClazzTag => - h = typeHash(c.typeValue, h) + // Go through `apiType` to get a value with a stable hash, it'd + // be better to use Murmur here too instead of relying on + // `hashCode`, but that would essentially mean duplicating + // https://github.com/sbt/zinc/blob/develop/internal/zinc-apiinfo/src/main/scala/xsbt/api/HashAPI.scala + // and at that point we might as well do type hashing on our own + // representation. + h = MurmurHash3.mix(h, apiType(c.typeValue).hashCode) case _ => h = MurmurHash3.mix(h, c.value.hashCode) h @@ -758,7 +742,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { p match case ref: RefTree @unchecked => val sym = ref.symbol - if sym.is(Inline, butNot = Param) && !seenInlineCache(inlineOrigin).contains(sym) then + if sym.is(Inline, butNot = Param) && !seenInlineCache.contains(sym) then // An inline method that calls another inline method will eventually inline the call // at a non-inline callsite, in this case if the implementation of the nested call // changes, then the callsite will have a different API, we should hash the definition @@ -824,20 +808,10 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { MurmurHash3.finalizeHash(h, len) end hashTparamsExtras - private def hashList(extraHashes: List[Int => Int])(initHash: Int): Int = - var h = initHash - var fs = extraHashes - var len = 0 - while fs.nonEmpty do - h = fs.head(h) - fs = fs.tail - len += 1 - MurmurHash3.finalizeHash(h, len) - /** Mix in the name hash also because otherwise switching which * parameter is inline will not affect the hash. */ - private def hashInlineParam(p: Symbol)(h: Int) = + private def hashInlineParam(p: Symbol, h: Int) = MurmurHash3.mix(p.name.toString.hashCode, MurmurHash3.mix(h, InlineParamHash)) def apiAnnotation(annot: Annotation): api.Annotation = { From 683c4584de638d608b6a27164da5b538bc75d06a Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Wed, 22 Dec 2021 09:55:26 +0100 Subject: [PATCH 8/8] clarify comment about inlineBodyCache --- compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index 5bbc52e09a82..3e0e148f7101 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -152,7 +152,9 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { */ private val seenInlineCache = mutable.HashSet.empty[Symbol] - /** This cache is optional, it avoids recomputing hashes of inline "Body" annotations. */ + /** This cache is optional, it avoids recomputing hashes of inline "Body" annotations, + * e.g. when a concrete inline method is inherited by a subclass. + */ private val inlineBodyCache = mutable.HashMap.empty[Symbol, Int] private val allNonLocalClassesInSrc = new mutable.HashSet[xsbti.api.ClassLike] @@ -654,12 +656,6 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { // this method need to be recompiled. sbt has no way of tracking method // bodies, so we include the hash of the body of the method as part of the // signature we send to sbt. - // - // FIXME: The API of a class we send to Zinc includes the signatures of - // inherited methods, which means that we repeatedly compute the hash of - // an inline def in every class that extends its owner. To avoid this we - // could store the hash as an annotation when pickling an inline def - // and retrieve it here instead of computing it on the fly. def hash[U](inlineOrigin: Symbol): Int = assert(seenInlineCache.add(s)) // will fail if already seen, guarded by treeHash