Skip to content

Commit e54b7e3

Browse files
authored
Merge pull request #2133 from dotty-staging/fix/refinedtype-incremental
incremental compilation: Fix unstable name hashing for refined members
2 parents 80d1990 + 164a77f commit e54b7e3

File tree

1 file changed

+42
-6
lines changed

1 file changed

+42
-6
lines changed

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

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder
114114
private[this] val classLikeCache = new mutable.HashMap[ClassSymbol, api.ClassLike]
115115
/** This cache is optional, it avoids recomputing representations */
116116
private[this] val typeCache = new mutable.HashMap[Type, api.Type]
117+
/** This cache is necessary to avoid unstable name hashing when `typeCache` is present,
118+
* see the comment in the `RefinedType` case in `computeType`
119+
* The cache key is (api of RefinedType#parent, api of RefinedType#refinedInfo).
120+
*/
121+
private[this] val refinedTypeCache = new mutable.HashMap[(api.Type, api.Definition), api.Structure]
117122

118123
private[this] object Constants {
119124
val emptyStringArray = Array[String]()
@@ -349,7 +354,14 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder
349354
Constants.emptyType
350355
case tp: NamedType =>
351356
val sym = tp.symbol
352-
// Normalize package prefix to avoid instability of representation
357+
// A type can sometimes be represented by multiple different NamedTypes
358+
// (they will be `=:=` to each other, but not `==`), and the compiler
359+
// may choose to use any of these representation, there is no stability
360+
// guarantee. We avoid this instability by always normalizing the
361+
// prefix: if it's a package, if we didn't do this sbt might conclude
362+
// that some API changed when it didn't, leading to overcompilation
363+
// (recompiling more things than what is needed for incremental
364+
// compilation to be correct).
353365
val prefix = if (sym.owner.is(Package))
354366
sym.owner.thisType
355367
else
@@ -389,15 +401,39 @@ private class ExtractAPICollector(implicit val ctx: Context) extends ThunkHolder
389401
new api.TypeDeclaration(apiType(lo), apiType(hi),
390402
Array(), name, Constants.public, Constants.emptyModifiers, Array())
391403
}
392-
393-
val decl: Array[api.Definition] = rt.refinedInfo match {
404+
val decl = rt.refinedInfo match {
394405
case rinfo: TypeBounds =>
395-
Array(typeRefinement(name, rinfo))
406+
typeRefinement(name, rinfo)
396407
case _ =>
397408
ctx.debuglog(i"sbt-api: skipped structural refinement in $rt")
398-
Array()
409+
null
399410
}
400-
new api.Structure(strict2lzy(Array(parent)), strict2lzy(decl), strict2lzy(Array()))
411+
412+
// Aggressive caching for RefinedTypes: `typeCache` is enough as long as two
413+
// RefinedType are `==`, but this is only the case when their `refinedInfo`
414+
// are `==` and this is not always the case, consider:
415+
//
416+
// val foo: { type Bla = a.b.T }
417+
// val bar: { type Bla = a.b.T }
418+
//
419+
// The sbt API representations of `foo` and `bar` (let's call them `apiFoo`
420+
// and `apiBar`) will both be instances of `Structure`. If `typeCache` was
421+
// the only cache, then in some cases we would have `apiFoo eq apiBar` and
422+
// in other cases we would just have `apiFoo == apiBar` (this happens
423+
// because the dotty representation of `a.b.T` is unstable, see the comment
424+
// in the `NamedType` case above).
425+
//
426+
// The fact that we may or may not have `apiFoo eq apiBar` is more than
427+
// an optimisation issue: it will determine whether the sbt name hash for
428+
// `Bla` contains one or two entries (because sbt `NameHashing` will not
429+
// traverse both `apiFoo` and `apiBar` if they are `eq`), therefore the
430+
// name hash of `Bla` will be unstable, unless we make sure that
431+
// `apiFoo == apiBar` always imply `apiFoo eq apiBar`. This is what
432+
// `refinedTypeCache` is for.
433+
refinedTypeCache.getOrElseUpdate((parent, decl), {
434+
val adecl: Array[api.Definition] = if (decl == null) Array() else Array(decl)
435+
new api.Structure(strict2lzy(Array(parent)), strict2lzy(adecl), strict2lzy(Array()))
436+
})
401437
case tp: RecType =>
402438
apiType(tp.parent)
403439
case RecThis(recType) =>

0 commit comments

Comments
 (0)