Skip to content

Commit d54ed14

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

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

+662
-34
lines changed

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

+109-22
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,129 @@ 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 TypeBounds(lo, hi) =>
653+
h = MurmurHash3.mix(h, apiType(lo).hashCode)
654+
h = MurmurHash3.mix(h, apiType(hi).hashCode)
655+
case tp: Type =>
656+
h = MurmurHash3.mix(h, apiType(tp).hashCode)
657+
h
658+
end typeHash
659+
660+
def constantHash(c: Constant, initHash: Int): Int =
661+
var h = MurmurHash3.mix(initHash, c.tag)
662+
c.tag match
663+
case NullTag =>
664+
// No value to hash, the tag is enough.
665+
case ClazzTag =>
666+
h = typeHash(c.typeValue, h)
667+
case _ =>
668+
h = MurmurHash3.mix(h, c.value.hashCode)
669+
h
670+
end constantHash
671+
672+
/**An inline method that calls another inline method will eventually inline the call
673+
* at a non-inline callsite, in this case if the implementation of the nested call
674+
* changes, then the callsite will have a different API, we should hash the definition
675+
*/
676+
def inlineDefHash(ref: Symbol, initHash: Int): Int =
677+
var h = initHash
678+
679+
def paramssHash(paramss: List[List[Symbol]], initHash: Int): Int = paramss match
680+
case Nil :: paramss1 =>
681+
paramssHash(paramss1, MurmurHash3.mix(initHash, EmptyParamHash))
682+
case params :: paramss1 =>
683+
var h = initHash
684+
val paramsIt = params.iterator
685+
while paramsIt.hasNext do
686+
val param = paramsIt.next
687+
h = nameHash(param.name, h)
688+
h = typeHash(param.info, h)
689+
if param.is(Inline) then
690+
h = MurmurHash3.mix(h, InlineParamHash) // inline would change the generated code
691+
if param.isType then
692+
val variance = param.paramVarianceSign
693+
if variance != 0 then
694+
h = MurmurHash3.mix(h, variance)
695+
paramssHash(paramss1, h)
696+
case Nil =>
697+
initHash
698+
end paramssHash
699+
700+
h = paramssHash(ref.paramSymss, h)
701+
h = typeHash(ref.info.finalResultType, h)
702+
positionedHash(Inliner.bodyToInline(ref), h)
703+
end inlineDefHash
625704

626705
def positionedHash(p: ast.Positioned, initHash: Int): Int =
706+
var h = initHash
707+
627708
p match
628709
case p: WithLazyField[?] =>
629710
p.forceIfLazy
630711
case _ =>
712+
713+
p match
714+
case ref: RefTree @unchecked =>
715+
val sym = ref.symbol
716+
def err(): Int =
717+
internalError(i"Don't know how to produce a stable hash for inline reference `$ref`", ref.sourcePos)
718+
ref.name.toString.hashCode
719+
if sym.is(Inline, butNot = Param) && !seenInlines.contains(sym) then
720+
seenInlines += sym // dont re-enter hashing this ref
721+
if sym.is(Method) then // inline def
722+
if Inliner.hasBodyToInline(sym) then
723+
h = inlineDefHash(sym, h)
724+
else
725+
h = err()
726+
else // inline val
727+
sym.info.widenTermRefExpr.dealias.normalized match
728+
case ConstantType(c) =>
729+
h = constantHash(c, h)
730+
case tpe =>
731+
h = err()
732+
case _ =>
733+
631734
// FIXME: If `p` is a tree we should probably take its type into account
632735
// when hashing it, but producing a stable hash for a type is not trivial
633736
// since the same type might have multiple representations, for method
634737
// signatures this is already handled by `computeType` and the machinery
635738
// in Zinc that generates hashes from that, if we can reliably produce
636739
// stable hashes for types ourselves then we could bypass all that and
637740
// send Zinc hashes directly.
638-
val h = MurmurHash3.mix(initHash, p.productPrefix.hashCode)
741+
h = MurmurHash3.mix(h, p.productPrefix.hashCode)
639742
iteratorHash(p.productIterator, h)
640743
end positionedHash
641744

642745
def iteratorHash(it: Iterator[Any], initHash: Int): Int =
643-
import core.Constants._
644746
var h = initHash
645747
while it.hasNext do
646748
it.next() match
@@ -649,24 +751,9 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
649751
case xs: List[?] =>
650752
h = iteratorHash(xs.iterator, h)
651753
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)
754+
h = constantHash(c, h)
667755
case n: Name =>
668-
// The hashCode of the name itself is not stable across compiler instances
669-
h = MurmurHash3.mix(h, n.toString.hashCode)
756+
h = nameHash(n, h)
670757
case elem =>
671758
internalError(
672759
i"Don't know how to produce a stable hash for `$elem` of unknown class ${elem.getClass}",
@@ -691,6 +778,6 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
691778
// annotated @org.junit.Test).
692779
api.Annotation.of(
693780
apiType(annot.tree.tpe), // Used by sbt to find tests to run
694-
Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree).toString)))
781+
Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree, inlineSym = NoSymbol).toString)))
695782
}
696783
}

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 = 1229 // 201st prime
9+
inline val TypeNameHash = 1231 // 202nd prime
10+
inline val EmptyParamHash = 1237 // 203rd prime
11+
inline val InlineParamHash = 1249 // 204th 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)