11package dotty .tools .dotc
22package sbt
33
4- import ast .{Trees , tpd }
4+ import ast .{Positioned , Trees , tpd , untpd }
55import core ._
66import core .Decorators ._
77import Annotations ._
@@ -11,6 +11,7 @@ import Phases._
1111import Trees ._
1212import Types ._
1313import Symbols ._
14+ import Names ._
1415import NameOps ._
1516import NameKinds .DefaultGetterName
1617import typer .Inliner
@@ -582,13 +583,18 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
582583 val annots = new mutable.ListBuffer [api.Annotation ]
583584 val inlineBody = Inliner .bodyToInline(s)
584585 if (! inlineBody.isEmpty) {
585- // FIXME: If the body of an inlineable method changes, all the reverse
586- // dependencies of this method need to be recompiled. sbt has no way
587- // of tracking method bodies, so as a hack we include the printed
588- // tree of the method as part of the signature we send to sbt.
589- // To do this properly we would need a way to hash trees and types in
590- // dotty itself.
591- annots += marker(inlineBody.toString)
586+ // If the body of an inline def changes, all the reverse dependencies of
587+ // this method need to be recompiled. sbt has no way of tracking method
588+ // bodies, so we include the hash of the body of the method as part of the
589+ // signature we send to sbt.
590+ //
591+ // FIXME: The API of a class we send to Zinc includes the signatures of
592+ // inherited methods, which means that we repeatedly compute the hash of
593+ // an inline def in every class that extends its owner. To avoid this we
594+ // could store the hash as an annotation when pickling an inline def
595+ // and retrieve it here instead of computing it on the fly.
596+ val inlineBodyHash = treeHash(inlineBody)
597+ annots += marker(inlineBodyHash.toString)
592598 }
593599
594600 // In the Scala2 ExtractAPI phase we only extract annotations that extend
@@ -602,16 +608,66 @@ private class ExtractAPICollector(using Context) extends ThunkHolder {
602608 annots.toList
603609 }
604610
611+ /** Produce a hash for a tree that is as stable as possible:
612+ * it should stay the same across compiler runs, compiler instances,
613+ * JVMs, etc.
614+ */
615+ def treeHash (tree : Tree ): Int =
616+ import scala .util .hashing .MurmurHash3
617+
618+ def positionedHash (p : ast.Positioned , initHash : Int ): Int =
619+ p match
620+ case p : WithLazyField [? ] =>
621+ p.forceIfLazy
622+ case _ =>
623+ // FIXME: If `p` is a tree we should probably take its type into account
624+ // when hashing it, but producing a stable hash for a type is not trivial
625+ // since the same type might have multiple representations, for method
626+ // signatures this is already handled by `computeType` and the machinery
627+ // in Zinc that generates hashes from that, if we can reliably produce
628+ // stable hashes for types ourselves then we could bypass all that and
629+ // send Zinc hashes directly.
630+ val h = MurmurHash3 .mix(initHash, p.productPrefix.hashCode)
631+ iteratorHash(p.productIterator, h)
632+ end positionedHash
633+
634+ def iteratorHash (it : Iterator [Any ], initHash : Int ): Int =
635+ var h = initHash
636+ while it.hasNext do
637+ it.next() match
638+ case p : Positioned =>
639+ h = positionedHash(p, h)
640+ case xs : List [? ] =>
641+ h = iteratorHash(xs.iterator, h)
642+ case c : core.Constants .Constant =>
643+ h = MurmurHash3 .mix(h, c.tag)
644+ h = MurmurHash3 .mix(h, c.value.## ) // We can't use `value.hashCode` since value might be null
645+ case n : Name =>
646+ // The hashCode of the name itself is not stable across compiler instances
647+ h = MurmurHash3 .mix(h, n.toString.hashCode)
648+ case elem =>
649+ report.warning(
650+ i """ Internal error: Don't know how to produce a stable hash for ` $elem` of unknown class ${elem.getClass}
651+ |Incremental compilation might not work correctly. """ , tree.sourcePos)
652+ h = MurmurHash3 .mix(h, elem.toString.hashCode)
653+ h
654+ end iteratorHash
655+
656+ val seed = 4 // https://xkcd.com/221
657+ val h = positionedHash(tree, seed)
658+ MurmurHash3 .finalizeHash(h, 0 )
659+ end treeHash
660+
605661 def apiAnnotation (annot : Annotation ): api.Annotation = {
606- // FIXME: To faithfully extract an API we should extract the annotation tree,
607- // sbt instead wants us to extract the annotation type and its arguments,
608- // to do this properly we would need a way to hash trees and types in dotty itself,
609- // instead we use the raw string representation of the annotation tree.
610- // However, we still need to extract the annotation type in the way sbt expect
611- // because sbt uses this information to find tests to run (for example
612- // junit tests are annotated @org.junit.Test).
662+ // Like with inline defs, the whole body of the annotation and not just its
663+ // type is part of its API so we need to store its hash, but Zinc wants us
664+ // to extract the annotation type and its arguments, so we use a dummy
665+ // annotation argument to store the hash of the tree. We still need to
666+ // extract the annotation type in the way Zinc expects because sbt uses this
667+ // information to find tests to run (for example junit tests are
668+ // annotated @org.junit.Test).
613669 api.Annotation .of(
614670 apiType(annot.tree.tpe), // Used by sbt to find tests to run
615- Array (api.AnnotationArgument .of(" FULLTREE " , annot.tree.toString)))
671+ Array (api.AnnotationArgument .of(" TREE_HASH " , treeHash( annot.tree) .toString)))
616672 }
617673}
0 commit comments