Skip to content

Commit a4f2db8

Browse files
committed
fix #11861, mix in nested inline def or val
1 parent e031348 commit a4f2db8

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

+664
-39
lines changed

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

+111-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,130 @@ 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 inlineDefHash(ref: Symbol, 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+
if param.isType then
694+
val variance = param.paramVarianceSign
695+
if variance != 0 then
696+
h = MurmurHash3.mix(h, variance)
697+
paramssHash(paramss1, h)
698+
case Nil =>
699+
initHash
700+
end paramssHash
701+
702+
h = paramssHash(ref.paramSymss, h)
703+
h = typeHash(ref.info.finalResultType, h)
704+
positionedHash(Inliner.bodyToInline(ref), h)
705+
end inlineDefHash
706+
707+
def err(what: String, elem: Any, pos: Positioned, initHash: Int): Int =
708+
internalError(i"Don't know how to produce a stable hash for $what", pos.sourcePos)
709+
MurmurHash3.mix(initHash, elem.toString.hashCode)
625710

626711
def positionedHash(p: ast.Positioned, initHash: Int): Int =
712+
var h = initHash
713+
627714
p match
628715
case p: WithLazyField[?] =>
629716
p.forceIfLazy
630717
case _ =>
718+
719+
p match
720+
case ref: RefTree @unchecked =>
721+
val sym = ref.symbol
722+
if sym.is(Inline, butNot = Param) && !seenInlines.contains(sym) then
723+
seenInlines += sym // dont re-enter hashing this ref
724+
if sym.is(Method) then // inline def
725+
if Inliner.hasBodyToInline(sym) then
726+
h = inlineDefHash(sym, h)
727+
else
728+
h = err(i"inline method reference `${ref.name}`", ref.name, ref, h)
729+
else // inline val
730+
// before inlining, inline val can be any expression/type, after inlining it should be constant
731+
val tp = sym.info.widenTermRefExpr.dealias.normalized // copied from `constToLiteral`
732+
h = typeHash(tp, h)
733+
case _ =>
734+
631735
// FIXME: If `p` is a tree we should probably take its type into account
632736
// when hashing it, but producing a stable hash for a type is not trivial
633737
// since the same type might have multiple representations, for method
634738
// signatures this is already handled by `computeType` and the machinery
635739
// in Zinc that generates hashes from that, if we can reliably produce
636740
// stable hashes for types ourselves then we could bypass all that and
637741
// send Zinc hashes directly.
638-
val h = MurmurHash3.mix(initHash, p.productPrefix.hashCode)
742+
h = MurmurHash3.mix(h, p.productPrefix.hashCode)
639743
iteratorHash(p.productIterator, h)
640744
end positionedHash
641745

642746
def iteratorHash(it: Iterator[Any], initHash: Int): Int =
643-
import core.Constants._
644747
var h = initHash
645748
while it.hasNext do
646749
it.next() match
@@ -649,30 +752,11 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
649752
case xs: List[?] =>
650753
h = iteratorHash(xs.iterator, h)
651754
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)
755+
h = constantHash(c, h)
667756
case n: Name =>
668-
// The hashCode of the name itself is not stable across compiler instances
669-
h = MurmurHash3.mix(h, n.toString.hashCode)
757+
h = nameHash(n, h)
670758
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)
759+
h = err(i"`$elem` of unknown class ${elem.getClass}", elem, tree, h)
676760
h
677761
end iteratorHash
678762

@@ -691,6 +775,6 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
691775
// annotated @org.junit.Test).
692776
api.Annotation.of(
693777
apiType(annot.tree.tpe), // Used by sbt to find tests to run
694-
Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree).toString)))
778+
Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree, inlineSym = NoSymbol).toString)))
695779
}
696780
}

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)