Skip to content

Commit 6e89717

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

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

+671
-34
lines changed

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

Lines changed: 118 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import java.io.PrintWriter
2424
import xsbti.api.DefinitionType
2525

2626
import scala.collection.mutable
27+
import java.time.chrono.Era
2728

2829
/** This phase sends a representation of the API of classes to sbt via callbacks.
2930
*
@@ -599,7 +600,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
599600
// an inline def in every class that extends its owner. To avoid this we
600601
// could store the hash as an annotation when pickling an inline def
601602
// and retrieve it here instead of computing it on the fly.
602-
val inlineBodyHash = treeHash(inlineBody)
603+
val inlineBodyHash = treeHash(inlineBody, inlineSym = s)
603604
annots += marker(inlineBodyHash.toString)
604605
}
605606

@@ -620,27 +621,129 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
620621
* it should stay the same across compiler runs, compiler instances,
621622
* JVMs, etc.
622623
*/
623-
def treeHash(tree: Tree): Int =
624+
def treeHash(tree: Tree, inlineSym: Symbol): Int =
624625
import scala.util.hashing.MurmurHash3
626+
import core.Constants.*
627+
628+
val seenInlines = mutable.HashSet.empty[Symbol]
629+
630+
if inlineSym ne NoSymbol then
631+
seenInlines += inlineSym // do not hash twice a recursive def
632+
633+
def nameHash(n: Name, initHash: Int): Int =
634+
val h =
635+
if n.isTermName then
636+
MurmurHash3.mix(initHash, TermNameHash)
637+
else
638+
MurmurHash3.mix(initHash, TypeNameHash)
639+
640+
// The hashCode of the name itself is not stable across compiler instances
641+
MurmurHash3.mix(h, n.toString.hashCode)
642+
end nameHash
643+
644+
def typeHash(tp: Type, initHash: Int): Int =
645+
// Go through `apiType` to get a value with a stable hash, it'd
646+
// be better to use Murmur here too instead of relying on
647+
// `hashCode`, but that would essentially mean duplicating
648+
// https://github.com/sbt/zinc/blob/develop/internal/zinc-apiinfo/src/main/scala/xsbt/api/HashAPI.scala
649+
// and at that point we might as well do type hashing on our own
650+
// representation.
651+
var h = initHash
652+
tp match
653+
case TypeBounds(lo, hi) =>
654+
h = MurmurHash3.mix(h, apiType(lo).hashCode)
655+
h = MurmurHash3.mix(h, apiType(hi).hashCode)
656+
case tp: Type =>
657+
h = MurmurHash3.mix(h, apiType(tp).hashCode)
658+
h
659+
end typeHash
660+
661+
def constantHash(c: Constant, initHash: Int): Int =
662+
var h = MurmurHash3.mix(initHash, c.tag)
663+
c.tag match
664+
case NullTag =>
665+
// No value to hash, the tag is enough.
666+
case ClazzTag =>
667+
h = typeHash(c.typeValue, h)
668+
case _ =>
669+
h = MurmurHash3.mix(h, c.value.hashCode)
670+
h
671+
end constantHash
672+
673+
/**An inline method that calls another inline method will eventually inline the call
674+
* at a non-inline callsite, in this case if the implementation of the nested call
675+
* changes, then the callsite will have a different API, we should hash the definition
676+
*/
677+
def inlineDefHash(ref: Symbol, initHash: Int): Int =
678+
var h = initHash
679+
680+
def paramssHash(paramss: List[List[Symbol]], initHash: Int): Int = paramss match
681+
case Nil :: paramss1 =>
682+
paramssHash(paramss1, MurmurHash3.mix(initHash, EmptyParamHash))
683+
case params :: paramss1 =>
684+
var h = initHash
685+
val paramsIt = params.iterator
686+
while paramsIt.hasNext do
687+
val param = paramsIt.next
688+
h = nameHash(param.name, h)
689+
h = typeHash(param.info, h)
690+
if param.is(Inline) then
691+
h = MurmurHash3.mix(h, InlineParamHash) // inline would change the generated code
692+
if param.isType then
693+
val variance = param.paramVarianceSign
694+
if variance != 0 then
695+
h = MurmurHash3.mix(h, variance)
696+
paramssHash(paramss1, h)
697+
case Nil =>
698+
initHash
699+
end paramssHash
700+
701+
h = paramssHash(ref.paramSymss, h)
702+
h = typeHash(ref.info.finalResultType, h)
703+
positionedHash(Inliner.bodyToInline(ref), h)
704+
end inlineDefHash
625705

626706
def positionedHash(p: ast.Positioned, initHash: Int): Int =
707+
var h = initHash
708+
627709
p match
628710
case p: WithLazyField[?] =>
629711
p.forceIfLazy
630712
case _ =>
713+
714+
p match
715+
case ref: RefTree @unchecked =>
716+
val sym = ref.symbol
717+
def err(): Int =
718+
internalError(i"Don't know how to produce a stable hash for inline reference `$ref`", ref.sourcePos)
719+
ref.name.toString.hashCode
720+
if sym.is(Inline, butNot = Param) && !seenInlines.contains(sym) then
721+
seenInlines += sym // dont re-enter hashing this ref
722+
if sym.is(Method) then // inline def
723+
if Inliner.hasBodyToInline(sym) then
724+
h = inlineDefHash(sym, h)
725+
else
726+
h = err()
727+
else // inline val
728+
sym.info.widenTermRefExpr.dealias.normalized match
729+
case ConstantType(c) =>
730+
h = constantHash(c, h)
731+
case tpe =>
732+
h = err()
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,24 +752,9 @@ 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 =>
671759
internalError(
672760
i"Don't know how to produce a stable hash for `$elem` of unknown class ${elem.getClass}",
@@ -676,6 +764,14 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
676764
h
677765
end iteratorHash
678766

767+
def typeIteratorHash(it: Iterator[Type], initHash: Int): Int =
768+
var h = initHash
769+
while it.hasNext do
770+
val apiValue = apiType(it.next) // see comment in constantHash for ClazzTag hashing
771+
h = MurmurHash3.mix(h, apiValue.hashCode)
772+
h
773+
end typeIteratorHash
774+
679775
val seed = 4 // https://xkcd.com/221
680776
val h = positionedHash(tree, seed)
681777
MurmurHash3.finalizeHash(h, 0)
@@ -691,6 +787,6 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
691787
// annotated @org.junit.Test).
692788
api.Annotation.of(
693789
apiType(annot.tree.tpe), // Used by sbt to find tests to run
694-
Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree).toString)))
790+
Array(api.AnnotationArgument.of("TREE_HASH", treeHash(annot.tree, inlineSym = NoSymbol).toString)))
695791
}
696792
}

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

Lines changed: 5 additions & 0 deletions
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) =
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 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
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+
}

0 commit comments

Comments
 (0)