Skip to content

Commit a96bf28

Browse files
committed
fix scala#11861, mix in nested inline def
1 parent a0fb338 commit a96bf28

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+549
-32
lines changed

compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
599599
// an inline def in every class that extends its owner. To avoid this we
600600
// could store the hash as an annotation when pickling an inline def
601601
// and retrieve it here instead of computing it on the fly.
602-
val inlineBodyHash = treeHash(inlineBody)
602+
val inlineBodyHash = treeHash(inlineBody, inlineSym = s)
603603
annots += marker(inlineBodyHash.toString)
604604
}
605605

@@ -620,27 +620,93 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
620620
* it should stay the same across compiler runs, compiler instances,
621621
* JVMs, etc.
622622
*/
623-
def treeHash(tree: Tree): Int =
623+
def treeHash(tree: Tree, inlineSym: Symbol): Int =
624624
import scala.util.hashing.MurmurHash3
625+
import core.Constants.*
626+
627+
val seenInlines = mutable.HashSet.empty[Symbol]
628+
629+
if inlineSym ne NoSymbol then
630+
seenInlines += inlineSym // do not hash twice a recursive def
631+
632+
inline def constantHash(c: Constant, initHash: Int): Int =
633+
var h = MurmurHash3.mix(initHash, c.tag)
634+
c.tag match
635+
case NullTag =>
636+
// No value to hash, the tag is enough.
637+
case ClazzTag =>
638+
// Go through `apiType` to get a value with a stable hash, it'd
639+
// be better to use Murmur here too instead of relying on
640+
// `hashCode`, but that would essentially mean duplicating
641+
// https://github.com/sbt/zinc/blob/develop/internal/zinc-apiinfo/src/main/scala/xsbt/api/HashAPI.scala
642+
// and at that point we might as well do type hashing on our own
643+
// representation.
644+
val apiValue = apiType(c.typeValue)
645+
h = MurmurHash3.mix(h, apiValue.hashCode)
646+
case _ =>
647+
h = MurmurHash3.mix(h, c.value.hashCode)
648+
h
649+
end constantHash
650+
651+
/**An inline method that calls another inline method will eventually inline the call
652+
* at a non-inline callsite, in this case if the implementation of the nested call
653+
* changes, then the callsite will have a different API, we should hash the definition
654+
*/
655+
def inlineRefHash(ref: Symbol, initHash: Int): Int =
656+
var h = initHash
657+
ref.defTree match // TODO: is this stable?
658+
case tree: Trees.DefDef[?] =>
659+
h = iteratorHash(tree.paramss.iterator, h)
660+
h = positionedHash(tree.tpt, h)
661+
case _ =>
662+
positionedHash(Inliner.bodyToInline(ref), h)
663+
end inlineRefHash
625664

626665
def positionedHash(p: ast.Positioned, initHash: Int): Int =
666+
var h = initHash
667+
627668
p match
628669
case p: WithLazyField[?] =>
629670
p.forceIfLazy
630671
case _ =>
672+
673+
p match
674+
case ref: RefTree @unchecked =>
675+
val sym = ref.symbol
676+
def err() =
677+
internalError(i"Don't know how to produce a stable hash for inline reference `$ref`", ref.sourcePos)
678+
if sym.is(Inline, butNot = Param) && !seenInlines.contains(sym) then
679+
seenInlines += sym // dont re-enter hashing this ref
680+
if sym.is(Method) then // inline def
681+
if Inliner.hasBodyToInline(sym) then
682+
h = inlineRefHash(sym, h)
683+
else
684+
err()
685+
else // inline val
686+
sym.info.widenTermRefExpr.dealias.normalized match
687+
case ConstantType(c) =>
688+
h = constantHash(c, h)
689+
case _ =>
690+
err()
691+
case _ =>
692+
693+
p match
694+
case param: Trees.ValDef[?] if param.symbol.isAllOf(InlineParam) =>
695+
h = MurmurHash3.mix(h, InlineParamHash)
696+
case _ =>
697+
631698
// FIXME: If `p` is a tree we should probably take its type into account
632699
// when hashing it, but producing a stable hash for a type is not trivial
633700
// since the same type might have multiple representations, for method
634701
// signatures this is already handled by `computeType` and the machinery
635702
// in Zinc that generates hashes from that, if we can reliably produce
636703
// stable hashes for types ourselves then we could bypass all that and
637704
// send Zinc hashes directly.
638-
val h = MurmurHash3.mix(initHash, p.productPrefix.hashCode)
705+
h = MurmurHash3.mix(h, p.productPrefix.hashCode)
639706
iteratorHash(p.productIterator, h)
640707
end positionedHash
641708

642709
def iteratorHash(it: Iterator[Any], initHash: Int): Int =
643-
import core.Constants._
644710
var h = initHash
645711
while it.hasNext do
646712
it.next() match
@@ -649,21 +715,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
649715
case xs: List[?] =>
650716
h = iteratorHash(xs.iterator, h)
651717
case c: Constant =>
652-
h = MurmurHash3.mix(h, c.tag)
653-
c.tag match
654-
case NullTag =>
655-
// No value to hash, the tag is enough.
656-
case ClazzTag =>
657-
// Go through `apiType` to get a value with a stable hash, it'd
658-
// be better to use Murmur here too instead of relying on
659-
// `hashCode`, but that would essentially mean duplicating
660-
// https://github.com/sbt/zinc/blob/develop/internal/zinc-apiinfo/src/main/scala/xsbt/api/HashAPI.scala
661-
// and at that point we might as well do type hashing on our own
662-
// representation.
663-
val apiValue = apiType(c.typeValue)
664-
h = MurmurHash3.mix(h, apiValue.hashCode)
665-
case _ =>
666-
h = MurmurHash3.mix(h, c.value.hashCode)
718+
h = constantHash(c, h)
667719
case n: Name =>
668720
// The hashCode of the name itself is not stable across compiler instances
669721
h = MurmurHash3.mix(h, n.toString.hashCode)
@@ -691,6 +743,6 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
691743
// annotated @org.junit.Test).
692744
api.Annotation.of(
693745
apiType(annot.tree.tpe), // Used by sbt to find tests to run
694-
Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree).toString)))
746+
Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree, inlineSym = NoSymbol).toString)))
695747
}
696748
}

compiler/src/dotty/tools/dotc/sbt/package.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import dotty.tools.dotc.core.Symbols.Symbol
55
import dotty.tools.dotc.core.NameOps.stripModuleClassSuffix
66
import dotty.tools.dotc.core.Names.Name
77

8+
inline val InlineParamHash = 1231 // aka true.##
9+
810
extension (sym: Symbol)
911

1012
def constructorName(using Context) =
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object A {
2+
3+
inline def callInline: Any = B.inlinedAny("yyy")
4+
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object B {
2+
3+
inline def inlinedAny(x: String): x.type = x
4+
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class C {
2+
val n = A.callInline
3+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import sbt.internal.inc.Analysis
2+
import complete.DefaultParsers._
3+
4+
// Reset compiler iterations, necessary because tests run in batch mode
5+
val recordPreviousIterations = taskKey[Unit]("Record previous iterations.")
6+
recordPreviousIterations := {
7+
val log = streams.value.log
8+
CompileState.previousIterations = {
9+
val previousAnalysis = (previousCompile in Compile).value.analysis.asScala
10+
previousAnalysis match {
11+
case None =>
12+
log.info("No previous analysis detected")
13+
0
14+
case Some(a: Analysis) => a.compilations.allCompilations.size
15+
}
16+
}
17+
}
18+
19+
val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.")
20+
21+
checkIterations := {
22+
val expected: Int = (Space ~> NatBasic).parsed
23+
val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations
24+
assert(expected == actual, s"Expected $expected compilations, got $actual")
25+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object B {
2+
3+
inline def inlinedAny(inline x: String): x.type = x
4+
5+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// This is necessary because tests are run in batch mode
2+
object CompileState {
3+
@volatile var previousIterations: Int = -1
4+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import sbt._
2+
import Keys._
3+
4+
object DottyInjectedPlugin extends AutoPlugin {
5+
override def requires = plugins.JvmPlugin
6+
override def trigger = allRequirements
7+
8+
override val projectSettings = Seq(
9+
scalaVersion := sys.props("plugin.scalaVersion")
10+
)
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
> compile
2+
> recordPreviousIterations
3+
# Force recompilation of A because B.inlinedAny, called by A.callInline, has changed
4+
# the type of its parameters.
5+
$ copy-file changes/B1.scala B.scala
6+
> compile
7+
# 1 to recompile B, then 1 more to recompile A due to B.inlinedAny change,
8+
# then 1 final compilation to recompile C due to A.callInline change
9+
> checkIterations 3
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object A {
2+
3+
inline def callInline: Any = B.inlinedAny("yyy")
4+
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object B {
2+
3+
inline def inlinedAny(x: String): x.type = x
4+
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class C {
2+
val n = A.callInline
3+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import sbt.internal.inc.Analysis
2+
import complete.DefaultParsers._
3+
4+
// Reset compiler iterations, necessary because tests run in batch mode
5+
val recordPreviousIterations = taskKey[Unit]("Record previous iterations.")
6+
recordPreviousIterations := {
7+
val log = streams.value.log
8+
CompileState.previousIterations = {
9+
val previousAnalysis = (previousCompile in Compile).value.analysis.asScala
10+
previousAnalysis match {
11+
case None =>
12+
log.info("No previous analysis detected")
13+
0
14+
case Some(a: Analysis) => a.compilations.allCompilations.size
15+
}
16+
}
17+
}
18+
19+
val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.")
20+
21+
checkIterations := {
22+
val expected: Int = (Space ~> NatBasic).parsed
23+
val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations
24+
assert(expected == actual, s"Expected $expected compilations, got $actual")
25+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object B {
2+
3+
inline def inlinedAny(x: Any): x.type = x
4+
5+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// This is necessary because tests are run in batch mode
2+
object CompileState {
3+
@volatile var previousIterations: Int = -1
4+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import sbt._
2+
import Keys._
3+
4+
object DottyInjectedPlugin extends AutoPlugin {
5+
override def requires = plugins.JvmPlugin
6+
override def trigger = allRequirements
7+
8+
override val projectSettings = Seq(
9+
scalaVersion := sys.props("plugin.scalaVersion")
10+
)
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
> compile
2+
> recordPreviousIterations
3+
# Force recompilation of A because B.inlinedAny, called by A.callInline, has changed
4+
# the type of its parameters.
5+
$ copy-file changes/B1.scala B.scala
6+
> compile
7+
# 1 to recompile B, then 1 more to recompile A due to B.inlinedAny change,
8+
# then 1 final compilation to recompile C due to A.callInline change
9+
> checkIterations 3
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object A {
2+
3+
inline def callInline: Any = B.inlinedAny("yyy")
4+
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object B {
2+
3+
transparent inline def inlinedAny(x: String): x.type = x
4+
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class C {
2+
val n = A.callInline
3+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import sbt.internal.inc.Analysis
2+
import complete.DefaultParsers._
3+
4+
// Reset compiler iterations, necessary because tests run in batch mode
5+
val recordPreviousIterations = taskKey[Unit]("Record previous iterations.")
6+
recordPreviousIterations := {
7+
val log = streams.value.log
8+
CompileState.previousIterations = {
9+
val previousAnalysis = (previousCompile in Compile).value.analysis.asScala
10+
previousAnalysis match {
11+
case None =>
12+
log.info("No previous analysis detected")
13+
0
14+
case Some(a: Analysis) => a.compilations.allCompilations.size
15+
}
16+
}
17+
}
18+
19+
val checkIterations = inputKey[Unit]("Verifies the accumulated number of iterations of incremental compilation.")
20+
21+
checkIterations := {
22+
val expected: Int = (Space ~> NatBasic).parsed
23+
val actual: Int = ((compile in Compile).value match { case a: Analysis => a.compilations.allCompilations.size }) - CompileState.previousIterations
24+
assert(expected == actual, s"Expected $expected compilations, got $actual")
25+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object B {
2+
3+
transparent inline def inlinedAny(x: String): String = x
4+
5+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// This is necessary because tests are run in batch mode
2+
object CompileState {
3+
@volatile var previousIterations: Int = -1
4+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import sbt._
2+
import Keys._
3+
4+
object DottyInjectedPlugin extends AutoPlugin {
5+
override def requires = plugins.JvmPlugin
6+
override def trigger = allRequirements
7+
8+
override val projectSettings = Seq(
9+
scalaVersion := sys.props("plugin.scalaVersion")
10+
)
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
> compile
2+
> recordPreviousIterations
3+
# Force recompilation of A because B.inlinedAny, called by A.callInline, has changed
4+
# the type of its parameters.
5+
$ copy-file changes/B1.scala B.scala
6+
> compile
7+
# 1 to recompile B, then 1 more to recompile A due to B.inlinedAny change,
8+
# then 1 final compilation to recompile C due to A.callInline change
9+
> checkIterations 3
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object A {
2+
3+
inline def callInline: Any = B.inlinedAny("yyy")
4+
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object B {
2+
3+
inline def inlinedAny(x: String): x.type = x
4+
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class C {
2+
val n = A.callInline
3+
}

0 commit comments

Comments
 (0)