Skip to content

Commit 856d416

Browse files
committed
Move notification of non-local classes to API
Registers only non-local generated classes in the callback by extracting information about its names and using the names to generate class file paths. Mimics the previous logic that was present in `Analyzer`, despite the fact that now we construct the names that the compiler will give to every non-local class independently of genbcode. Why do we do this? The motivation is that we want to run the incremental algorithm independently of the compiler pipeline. This independence enables us to: 1. Offload the incremental compiler logic out of the primary pipeline and run the incremental phases concurrently. 2. Know before the compilation is completed whether another compilation will or will not be required. This is important to make incremental compilation work with pipelining and enables further optimizations; for example, we can start subsequent incremental compilations before (!) the initial compilation is done. This can buy us ~30-40% faster incremental compiler iterations. This method only takes care of non-local classes because local clsases have no relevance in the correctness of the algorithm and can be registered after genbcode. Local classes are only used to contruct the relations of products and to produce the list of generated files + stamps, but names referring to local classes **never** show up in the name hashes of classes' APIs, hence never considered for name hashing. As local class files are owned by other classes that change whenever they change, we could most likely live without adding their class files to the products relation and registering their stamps. However, to be on the safe side, we will continue to register the local products in `Analyzer`.
1 parent 49031d4 commit 856d416

File tree

4 files changed

+152
-21
lines changed

4 files changed

+152
-21
lines changed

internal/compiler-bridge/src/main/scala/xsbt/API.scala

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,31 @@ object API {
1515
val name = "xsbt-api"
1616
}
1717

18-
final class API(val global: CallbackGlobal) extends Compat with GlobalHelpers {
18+
final class API(val global: CallbackGlobal) extends Compat with GlobalHelpers with ClassName {
1919
import global._
2020

21+
import scala.collection.mutable
22+
private val nonLocalClassSymbolsInCurrentUnits = new mutable.HashSet[Symbol]()
23+
2124
def newPhase(prev: Phase) = new ApiPhase(prev)
2225
class ApiPhase(prev: Phase) extends GlobalPhase(prev) {
2326
override def description = "Extracts the public API from source files."
2427
def name = API.name
2528
override def run(): Unit = {
2629
val start = System.currentTimeMillis
2730
super.run()
31+
32+
// After processing all units, register generated classes
33+
registerGeneratedClasses(nonLocalClassSymbolsInCurrentUnits.iterator)
34+
nonLocalClassSymbolsInCurrentUnits.clear()
35+
2836
callback.apiPhaseCompleted()
2937
val stop = System.currentTimeMillis
3038
debuglog("API phase took : " + ((stop - start) / 1000.0) + " s")
3139
}
3240

3341
def apply(unit: global.CompilationUnit): Unit = processUnit(unit)
34-
3542
private def processUnit(unit: CompilationUnit) = if (!unit.isJava) processScalaUnit(unit)
36-
3743
private def processScalaUnit(unit: CompilationUnit): Unit = {
3844
val sourceFile = unit.source.file.file
3945
debuglog("Traversing " + sourceFile)
@@ -59,6 +65,133 @@ final class API(val global: CallbackGlobal) extends Compat with GlobalHelpers {
5965
while (mainClassesIt.hasNext) {
6066
callback.mainClass(sourceFile, mainClassesIt.next())
6167
}
68+
69+
extractApi.allExtractedNonLocalSymbols.foreach { cs =>
70+
// Only add the class symbols defined in this compilation unit
71+
if (cs.sourceFile != null) nonLocalClassSymbolsInCurrentUnits.+=(cs)
72+
}
73+
}
74+
}
75+
76+
private case class FlattenedNames(binaryName: String, className: String)
77+
78+
/**
79+
* Replicate the behaviour of `fullName` with a few changes to the code to produce
80+
* correct file-system compatible full names for non-local classes. It mimics the
81+
* paths of the class files produced by genbcode.
82+
*
83+
* Changes compared to the normal version in the compiler:
84+
*
85+
* 1. It will use the encoded name instead of the normal name.
86+
* 2. It will not skip the name of the package object class (required for the class file path).
87+
*
88+
* Note that using `javaBinaryName` is not useful for these symbols because we
89+
* need the encoded names. Zinc keeps track of encoded names in both the binary
90+
* names and the Zinc names.
91+
*
92+
* @param symbol The symbol for which we extract the full name.
93+
* @param separator The separator that we will apply between every name.
94+
* @param suffix The suffix to add at the end (in case it's a module).
95+
* @param includePackageObjectClassNames Include package object class names or not.
96+
* @return The full name.
97+
*/
98+
def fullName(
99+
symbol: Symbol,
100+
separator: Char,
101+
suffix: CharSequence,
102+
includePackageObjectClassNames: Boolean
103+
): String = {
104+
var b: java.lang.StringBuffer = null
105+
def loop(size: Int, sym: Symbol): Unit = {
106+
val symName = sym.name
107+
// Use of encoded to produce correct paths for names that have symbols
108+
val encodedName = symName.encoded
109+
val nSize = encodedName.length - (if (symName.endsWith(nme.LOCAL_SUFFIX_STRING)) 1 else 0)
110+
if (sym.isRoot || sym.isRootPackage || sym == NoSymbol || sym.owner.isEffectiveRoot) {
111+
val capacity = size + nSize
112+
b = new java.lang.StringBuffer(capacity)
113+
b.append(chrs, symName.start, nSize)
114+
} else {
115+
val next = if (sym.owner.isPackageObjectClass) sym.owner else sym.effectiveOwner.enclClass
116+
loop(size + nSize + 1, next)
117+
// Addition to normal `fullName` to produce correct names for nested non-local classes
118+
if (sym.isNestedClass) b.append(nme.MODULE_SUFFIX_STRING) else b.append(separator)
119+
b.append(chrs, symName.start, nSize)
120+
}
121+
}
122+
loop(suffix.length(), symbol)
123+
b.append(suffix)
124+
b.toString
125+
}
126+
127+
/**
128+
* Registers only non-local generated classes in the callback by extracting
129+
* information about its names and using the names to generate class file paths.
130+
*
131+
* Mimics the previous logic that was present in `Analyzer`, despite the fact
132+
* that now we construct the names that the compiler will give to every non-local
133+
* class independently of genbcode.
134+
*
135+
* Why do we do this? The motivation is that we want to run the incremental algorithm
136+
* independently of the compiler pipeline. This independence enables us to:
137+
*
138+
* 1. Offload the incremental compiler logic out of the primary pipeline and
139+
* run the incremental phases concurrently.
140+
* 2. Know before the compilation is completed whether another compilation will or
141+
* will not be required. This is important to make incremental compilation work
142+
* with pipelining and enables further optimizations; for example, we can start
143+
* subsequent incremental compilations before (!) the initial compilation is done.
144+
* This can buy us ~30-40% faster incremental compiler iterations.
145+
*
146+
* This method only takes care of non-local classes because local clsases have no
147+
* relevance in the correctness of the algorithm and can be registered after genbcode.
148+
* Local classes are only used to contruct the relations of products and to produce
149+
* the list of generated files + stamps, but names referring to local classes **never**
150+
* show up in the name hashes of classes' APIs, hence never considered for name hashing.
151+
*
152+
* As local class files are owned by other classes that change whenever they change,
153+
* we could most likely live without adding their class files to the products relation
154+
* and registering their stamps. However, to be on the safe side, we will continue to
155+
* register the local products in `Analyzer`.
156+
*
157+
* @param allClassSymbols The class symbols found in all the compilation units.
158+
*/
159+
def registerGeneratedClasses(classSymbols: Iterator[Symbol]): Unit = {
160+
classSymbols.foreach { symbol =>
161+
val sourceFile = symbol.sourceFile
162+
val sourceJavaFile =
163+
if (sourceFile == null) symbol.enclosingTopLevelClass.sourceFile.file else sourceFile.file
164+
165+
def registerProductNames(names: FlattenedNames): Unit = {
166+
// Guard against a local class in case it surreptitiously leaks here
167+
if (!symbol.isLocalClass) {
168+
val classFileName = s"${names.binaryName}.class"
169+
val outputDir = global.settings.outputDirs.outputDirFor(sourceFile).file
170+
val classFile = new java.io.File(outputDir, classFileName)
171+
val zincClassName = names.className
172+
val srcClassName = classNameAsString(symbol)
173+
callback.generatedNonLocalClass(sourceJavaFile, classFile, zincClassName, srcClassName)
174+
} else ()
175+
}
176+
177+
val names = FlattenedNames(
178+
//flattenedNames(symbol)
179+
fullName(symbol, java.io.File.separatorChar, symbol.moduleSuffix, true),
180+
fullName(symbol, '.', symbol.moduleSuffix, false)
181+
)
182+
183+
registerProductNames(names)
184+
185+
// Register the names of top-level module symbols that emit two class files
186+
val isTopLevelUniqueModule =
187+
symbol.owner.isPackageClass && symbol.isModuleClass && symbol.companionClass == NoSymbol
188+
if (isTopLevelUniqueModule || symbol.isPackageObject) {
189+
val names = FlattenedNames(
190+
fullName(symbol, java.io.File.separatorChar, "", true),
191+
fullName(symbol, '.', "", false)
192+
)
193+
registerProductNames(names)
194+
}
62195
}
63196
}
64197

internal/compiler-bridge/src/main/scala/xsbt/Analyzer.scala

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import scala.tools.nsc.Phase
1212
object Analyzer {
1313
def name = "xsbt-analyzer"
1414
}
15+
1516
final class Analyzer(val global: CallbackGlobal) extends LocateClassFile {
1617
import global._
1718

@@ -20,34 +21,25 @@ final class Analyzer(val global: CallbackGlobal) extends LocateClassFile {
2021
override def description =
2122
"Finds concrete instances of provided superclasses, and application entry points."
2223
def name = Analyzer.name
24+
2325
def apply(unit: CompilationUnit): Unit = {
2426
if (!unit.isJava) {
2527
val sourceFile = unit.source.file.file
26-
// build list of generated classes
2728
for (iclass <- unit.icode) {
2829
val sym = iclass.symbol
30+
val outputDir = settings.outputDirs.outputDirFor(sym.sourceFile).file
2931
def addGenerated(separatorRequired: Boolean): Unit = {
30-
for (classFile <- outputDirs map (fileForClass(_, sym, separatorRequired)) find (_.exists)) {
32+
val classFile = fileForClass(outputDir, sym, separatorRequired)
33+
if (classFile.exists()) {
3134
assert(sym.isClass, s"${sym.fullName} is not a class")
32-
// we would like to use Symbol.isLocalClass but that relies on Symbol.owner which
33-
// is lost at this point due to lambdalift
34-
// the LocalNonLocalClass.isLocal can return None, which means, we're asking about
35-
// the class it has not seen before. How's that possible given we're performing a lookup
36-
// for every declared class in Dependency phase? We can have new classes introduced after
37-
// Dependency phase has ran. For example, the implementation classes for traits.
38-
val isLocalClass = localToNonLocalClass.isLocal(sym).getOrElse(true)
39-
if (!isLocalClass) {
40-
val srcClassName = classNameAsString(sym)
41-
val binaryClassName = flatclassName(sym, '.', separatorRequired)
42-
callback.generatedNonLocalClass(sourceFile,
43-
classFile,
44-
binaryClassName,
45-
srcClassName)
46-
} else {
35+
// Use own map of local classes computed before lambdalift to ascertain class locality
36+
if (localToNonLocalClass.isLocal(sym).getOrElse(true)) {
37+
// Inform callback about local classes, non-local classes have been reported in API
4738
callback.generatedLocalClass(sourceFile, classFile)
4839
}
4940
}
5041
}
42+
5143
if (sym.isModuleClass && !sym.isImplClass) {
5244
if (isTopLevelModule(sym) && sym.companionClass == NoSymbol)
5345
addGenerated(false)

internal/compiler-bridge/src/main/scala/xsbt/ExtractAPI.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class ExtractAPI[GlobalType <: Global](
7272

7373
private[this] val emptyStringArray = Array.empty[String]
7474

75+
private[this] val allNonLocalClassSymbols = perRunCaches.newSet[Symbol]()
7576
private[this] val allNonLocalClassesInSrc = perRunCaches.newSet[xsbti.api.ClassLike]()
7677
private[this] val _mainClasses = perRunCaches.newSet[String]()
7778

@@ -430,7 +431,8 @@ class ExtractAPI[GlobalType <: Global](
430431
def mkVar = Some(fieldDef(in, sym, keepConst = false, xsbti.api.Var.of(_, _, _, _, _)))
431432
def mkVal = Some(fieldDef(in, sym, keepConst = true, xsbti.api.Val.of(_, _, _, _, _)))
432433
if (isClass(sym))
433-
if (ignoreClass(sym)) None else Some(classLike(in, sym))
434+
if (ignoreClass(sym)) {allNonLocalClassSymbols.+=(sym); None}
435+
else Some(classLike(in, sym))
434436
else if (sym.isNonClassType)
435437
Some(typeDef(in, sym))
436438
else if (sym.isVariable)
@@ -646,6 +648,8 @@ class ExtractAPI[GlobalType <: Global](
646648
allNonLocalClassesInSrc.toSet
647649
}
648650

651+
def allExtractedNonLocalSymbols: Set[Symbol] = allNonLocalClassSymbols.toSet
652+
649653
def mainClasses: Set[String] = {
650654
forceStructures()
651655
_mainClasses.toSet
@@ -691,6 +695,7 @@ class ExtractAPI[GlobalType <: Global](
691695
val classWithMembers = constructClass(structure)
692696

693697
allNonLocalClassesInSrc += classWithMembers
698+
allNonLocalClassSymbols += sym
694699

695700
if (sym.isStatic && defType == DefinitionType.Module && definitions.hasJavaMainMethod(sym)) {
696701
_mainClasses += name

internal/compiler-bridge/src/main/scala/xsbt/LocalToNonLocalClass.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class LocalToNonLocalClass[G <: CallbackGlobal](val global: G) {
5757
assert(s.isClass, s"The ${s.fullName} is not a class.")
5858
cache.getOrElseUpdate(s, lookupNonLocal(s))
5959
}
60+
6061
private def lookupNonLocal(s: Symbol): Symbol = {
6162
if (s.owner.isPackageClass) s
6263
else if (s.owner.isClass) {

0 commit comments

Comments
 (0)