Skip to content

Commit af404e3

Browse files
committed
fix #11861, mix in nested inline def or val
1 parent 781e90a commit af404e3

Some content is hidden

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

70 files changed

+656
-39
lines changed

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

+103-27
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,122 @@ 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+
def nameHash(n: Name, initHash: Int): Int =
633+
val h =
634+
if n.isTermName then
635+
MurmurHash3.mix(initHash, TermNameHash)
636+
else
637+
MurmurHash3.mix(initHash, TypeNameHash)
638+
639+
// The hashCode of the name itself is not stable across compiler instances
640+
MurmurHash3.mix(h, n.toString.hashCode)
641+
end nameHash
642+
643+
def typeHash(tp: Type, initHash: Int): Int =
644+
// Go through `apiType` to get a value with a stable hash, it'd
645+
// be better to use Murmur here too instead of relying on
646+
// `hashCode`, but that would essentially mean duplicating
647+
// https://github.com/sbt/zinc/blob/develop/internal/zinc-apiinfo/src/main/scala/xsbt/api/HashAPI.scala
648+
// and at that point we might as well do type hashing on our own
649+
// representation.
650+
var h = initHash
651+
tp match
652+
case ConstantType(c) =>
653+
h = constantHash(c, h)
654+
case TypeBounds(lo, hi) =>
655+
h = MurmurHash3.mix(h, apiType(lo).hashCode)
656+
h = MurmurHash3.mix(h, apiType(hi).hashCode)
657+
case tp =>
658+
h = MurmurHash3.mix(h, apiType(tp).hashCode)
659+
h
660+
end typeHash
661+
662+
def constantHash(c: Constant, initHash: Int): Int =
663+
var h = MurmurHash3.mix(initHash, c.tag)
664+
c.tag match
665+
case NullTag =>
666+
// No value to hash, the tag is enough.
667+
case ClazzTag =>
668+
h = typeHash(c.typeValue, h)
669+
case _ =>
670+
h = MurmurHash3.mix(h, c.value.hashCode)
671+
h
672+
end constantHash
673+
674+
/**An inline method that calls another inline method will eventually inline the call
675+
* at a non-inline callsite, in this case if the implementation of the nested call
676+
* changes, then the callsite will have a different API, we should hash the definition
677+
*/
678+
def inlineReferenceHash(ref: Symbol, rhs: Tree, initHash: Int): Int =
679+
var h = initHash
680+
681+
def paramssHash(paramss: List[List[Symbol]], initHash: Int): Int = paramss match
682+
case Nil :: paramss1 =>
683+
paramssHash(paramss1, MurmurHash3.mix(initHash, EmptyParamHash))
684+
case params :: paramss1 =>
685+
var h = initHash
686+
val paramsIt = params.iterator
687+
while paramsIt.hasNext do
688+
val param = paramsIt.next
689+
h = nameHash(param.name, h)
690+
h = typeHash(param.info, h)
691+
if param.is(Inline) then
692+
h = MurmurHash3.mix(h, InlineParamHash) // inline would change the generated code
693+
paramssHash(paramss1, h)
694+
case Nil =>
695+
initHash
696+
end paramssHash
697+
698+
h = paramssHash(ref.paramSymss, h)
699+
h = typeHash(ref.info.finalResultType, h)
700+
positionedHash(rhs, h)
701+
end inlineReferenceHash
702+
703+
def err(what: String, elem: Any, pos: Positioned, initHash: Int): Int =
704+
internalError(i"Don't know how to produce a stable hash for $what", pos.sourcePos)
705+
MurmurHash3.mix(initHash, elem.toString.hashCode)
625706

626707
def positionedHash(p: ast.Positioned, initHash: Int): Int =
708+
var h = initHash
709+
627710
p match
628711
case p: WithLazyField[?] =>
629712
p.forceIfLazy
630713
case _ =>
714+
715+
p match
716+
case ref: RefTree @unchecked =>
717+
val sym = ref.symbol
718+
if sym.is(Inline, butNot = Param) && !seenInlines.contains(sym) then
719+
seenInlines += sym // dont re-enter hashing this ref
720+
sym.defTree match
721+
case defTree: ValOrDefDef =>
722+
h = inlineReferenceHash(sym, defTree.rhs, h)
723+
case _ =>
724+
h = err(i"inline method reference `${ref.name}`", ref.name, ref, h)
725+
case _ =>
726+
631727
// FIXME: If `p` is a tree we should probably take its type into account
632728
// when hashing it, but producing a stable hash for a type is not trivial
633729
// since the same type might have multiple representations, for method
634730
// signatures this is already handled by `computeType` and the machinery
635731
// in Zinc that generates hashes from that, if we can reliably produce
636732
// stable hashes for types ourselves then we could bypass all that and
637733
// send Zinc hashes directly.
638-
val h = MurmurHash3.mix(initHash, p.productPrefix.hashCode)
734+
h = MurmurHash3.mix(h, p.productPrefix.hashCode)
639735
iteratorHash(p.productIterator, h)
640736
end positionedHash
641737

642738
def iteratorHash(it: Iterator[Any], initHash: Int): Int =
643-
import core.Constants._
644739
var h = initHash
645740
while it.hasNext do
646741
it.next() match
@@ -649,30 +744,11 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
649744
case xs: List[?] =>
650745
h = iteratorHash(xs.iterator, h)
651746
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)
747+
h = constantHash(c, h)
667748
case n: Name =>
668-
// The hashCode of the name itself is not stable across compiler instances
669-
h = MurmurHash3.mix(h, n.toString.hashCode)
749+
h = nameHash(n, h)
670750
case elem =>
671-
internalError(
672-
i"Don't know how to produce a stable hash for `$elem` of unknown class ${elem.getClass}",
673-
tree.sourcePos)
674-
675-
h = MurmurHash3.mix(h, elem.toString.hashCode)
751+
h = err(i"`$elem` of unknown class ${elem.getClass}", elem, tree, h)
676752
h
677753
end iteratorHash
678754

@@ -691,6 +767,6 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
691767
// annotated @org.junit.Test).
692768
api.Annotation.of(
693769
apiType(annot.tree.tpe), // Used by sbt to find tests to run
694-
Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree).toString)))
770+
Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree, inlineSym = NoSymbol).toString)))
695771
}
696772
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ 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 TermNameHash = 1987 // 300th prime
9+
inline val TypeNameHash = 1993 // 301st prime
10+
inline val EmptyParamHash = 1997 // 302nd prime
11+
inline val InlineParamHash = 1999 // 303rd prime
12+
813
extension (sym: Symbol)
914

1015
def constructorName(using Context) =
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+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class C {
2+
val n = A.callInline
3+
}
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+
}
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+
}
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+
}
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+
}
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 added
4+
# the inline flag to one 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
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+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class C {
2+
val n = A.callInline
3+
}
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+
}
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+
}
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+
}
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+
}
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
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+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class C {
2+
val n = A.callInline
3+
}
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+
}
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+
}
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+
}

0 commit comments

Comments
 (0)