Skip to content

Commit

Permalink
Partial redesign of incremental compiler invalidation.
Browse files Browse the repository at this point in the history
We now do the right thing when packages are either newly created or deleted. Previously there was a problem when a new package was created inside a system package (and, unofrtunately, root is a system package). That's fixed now. We also approximate more tightly now when packages are newly created (iei the newly created symbol gets rescanned, instead of its owner).

Incremental class invalidation: dealing with empty package.

The compiler can now also invalidate the empty package. Previously, no invalidation was done because empty was identified with root, which is considered a system package.

(1) Fixed NPE when creating a new toplevel package in invalidation. (2) generalized interface to deal with multiple entries at a time.
  • Loading branch information
odersky committed May 30, 2012
1 parent 765aab6 commit e156d4a
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 81 deletions.
139 changes: 104 additions & 35 deletions src/compiler/scala/tools/nsc/Global.scala
Expand Up @@ -12,7 +12,7 @@ import scala.tools.util.PathResolver
import scala.collection.{ mutable, immutable } import scala.collection.{ mutable, immutable }
import io.{ SourceReader, AbstractFile, Path } import io.{ SourceReader, AbstractFile, Path }
import reporters.{ Reporter, ConsoleReporter } import reporters.{ Reporter, ConsoleReporter }
import util.{ NoPosition, Exceptional, ClassPath, SourceFile, NoSourceFile, Statistics, StatisticsInfo, BatchSourceFile, ScriptSourceFile, ScalaClassLoader, returning } import util.{ NoPosition, Exceptional, ClassPath, MergedClassPath, SourceFile, NoSourceFile, Statistics, StatisticsInfo, BatchSourceFile, ScriptSourceFile, ScalaClassLoader, returning }
import scala.reflect.internal.pickling.{ PickleBuffer, PickleFormat } import scala.reflect.internal.pickling.{ PickleBuffer, PickleFormat }
import settings.{ AestheticSettings } import settings.{ AestheticSettings }
import symtab.{ Flags, SymbolTable, SymbolLoaders, SymbolTrackers } import symtab.{ Flags, SymbolTable, SymbolLoaders, SymbolTrackers }
Expand Down Expand Up @@ -73,6 +73,7 @@ class Global(var currentSettings: Settings, var reporter: Reporter) extends Symb
else new { val global: Global.this.type = Global.this } with JavaPlatform else new { val global: Global.this.type = Global.this } with JavaPlatform


type PlatformClassPath = ClassPath[platform.BinaryRepr] type PlatformClassPath = ClassPath[platform.BinaryRepr]
type OptClassPath = Option[PlatformClassPath]


def classPath: PlatformClassPath = platform.classPath def classPath: PlatformClassPath = platform.classPath


Expand Down Expand Up @@ -879,20 +880,38 @@ class Global(var currentSettings: Settings, var reporter: Reporter) extends Symb
* - a list of of packages that should have been invalidated but were not because * - a list of of packages that should have been invalidated but were not because
* they are system packages. * they are system packages.
*/ */
def invalidateClassPathEntry(path: String): (List[Symbol], List[Symbol]) = { def invalidateClassPathEntries(paths: String*): (List[ClassSymbol], List[ClassSymbol]) = {
val invalidated, failed = new mutable.ListBuffer[Symbol] val invalidated, failed = new mutable.ListBuffer[ClassSymbol]
classPath match { classPath match {
case cp: util.MergedClassPath[_] => case cp: MergedClassPath[_] =>
val dir = AbstractFile getDirectory path def assoc(path: String): List[(PlatformClassPath, PlatformClassPath)] = {
val canonical = Some(dir.canonicalPath) val dir = AbstractFile getDirectory path
cp.entries find (_.origin == canonical) match { val canonical = dir.canonicalPath
case Some(oldEntry) => def matchesCanonical(e: ClassPath[_]) = e.origin match {
val newEntry = cp.context.newClassPath(dir) case Some(opath) =>
platform.updateClassPath(oldEntry, newEntry) (AbstractFile getDirectory opath).canonicalPath == canonical
informProgress(s"classpath updated to $classPath") case None =>
reSync(definitions.RootClass, classPath, oldEntry, newEntry, invalidated, failed) false
case None => }
error(s"cannot invalidate: no entry named $path in classpath $classPath") cp.entries find matchesCanonical match {
case Some(oldEntry) =>
List(oldEntry -> cp.context.newClassPath(dir))
case None =>
println(s"canonical = $canonical, origins = ${cp.entries map (_.origin)}")
error(s"cannot invalidate: no entry named $path in classpath $classPath")
List()
}
}
val subst = Map(paths flatMap assoc: _*)
if (subst.nonEmpty) {
platform updateClassPath subst
informProgress(s"classpath updated on entries [${subst.keys mkString ","}]")
def mkClassPath(elems: Iterable[PlatformClassPath]): PlatformClassPath =
if (elems.size == 1) elems.head
else new MergedClassPath(elems, classPath.context)
val oldEntries = mkClassPath(subst.keys)
val newEntries = mkClassPath(subst.values)
reSync(definitions.RootClass, Some(classPath), Some(oldEntries), Some(newEntries), invalidated, failed)
} }
} }
def show(msg: String, syms: collection.Traversable[Symbol]) = def show(msg: String, syms: collection.Traversable[Symbol]) =
Expand All @@ -903,35 +922,84 @@ class Global(var currentSettings: Settings, var reporter: Reporter) extends Symb
(invalidated.toList, failed.toList) (invalidated.toList, failed.toList)
} }


private def reSync(root: Symbol, all: PlatformClassPath, /** Re-syncs symbol table with classpath
oldEntry: PlatformClassPath, newEntry: PlatformClassPath, * @param root The root symbol to be resynced (a package class)
invalidated: mutable.ListBuffer[Symbol], failed: mutable.ListBuffer[Symbol]) { * @param allEntries Optionally, the corresponding package in the complete current classPath
ifDebug(informProgress(s"syncing $root, $oldEntry -> $newEntry")) * @param oldEntries Optionally, the corresponding package in the old classPath entries
* @param newEntries Optionally, the corresponding package in the new classPath entries
* @param invalidated A listbuffer collecting the invalidated package classes
* @param failed A listbuffer collecting system package classes which could not be invalidated
* The resyncing strategy is determined by the absence or presence of classes and packages.
* If either oldEntries or newEntries contains classes, root is invalidated, provided a corresponding package
* exists in allEntries, or otherwise is removed.
* Otherwise, the action is determined by the following matrix, with columns:
*
* old new all sym action
* + + + + recurse into all child packages of old ++ new
* + - + + invalidate root
* + - - + remove root from its scope
* - + + + invalidate root
* - + + - create and enter root
* - - * * no action
*
* Here, old, new, all mean classpaths and sym means symboltable. + is presence of an
* entry in its column, - is absence, * is don't care.
*
* Note that new <= all and old <= sym, so the matrix above covers all possibilities.
*/
private def reSync(root: ClassSymbol,
allEntries: OptClassPath, oldEntries: OptClassPath, newEntries: OptClassPath,
invalidated: mutable.ListBuffer[ClassSymbol], failed: mutable.ListBuffer[ClassSymbol]) {
ifDebug(informProgress(s"syncing $root, $oldEntries -> $newEntries"))

val getName: ClassPath[platform.BinaryRepr] => String = (_.name) val getName: ClassPath[platform.BinaryRepr] => String = (_.name)
val oldPackages = oldEntry.packages sortBy getName def hasClasses(cp: OptClassPath) = cp.isDefined && cp.get.classes.nonEmpty
val newPackages = newEntry.packages sortBy getName def invalidateOrRemove(root: ClassSymbol) = {
val hasChanged = allEntries match {
oldEntry.classes.nonEmpty || case Some(cp) => root setInfo new loaders.PackageLoader(cp)
newEntry.classes.nonEmpty || case None => root.owner.info.decls unlink root.sourceModule
(oldPackages map getName) != (newPackages map getName) }
if (hasChanged && !isSystemPackageClass(root)) {
root setInfo new loaders.PackageLoader(all)
invalidated += root invalidated += root
}
def packageNames(cp: PlatformClassPath): Set[String] = cp.packages.toSet map getName
def subPackage(cp: PlatformClassPath, name: String): OptClassPath =
cp.packages find (cp1 => getName(cp1) == name)

val classesFound = hasClasses(oldEntries) || hasClasses(newEntries)
if (classesFound && !isSystemPackageClass(root)) {
invalidateOrRemove(root)
} else { } else {
if (hasChanged) failed += root if (classesFound) {
for ((oldNested, newNested) <- oldPackages zip newPackages) { if (root.isRoot) invalidateOrRemove(definitions.EmptyPackageClass)
val pkgname = newNested.name else failed += root
val pkg = root.info decl newTermName(pkgname) }
val allNested = (all.packages find (_.name == pkgname)).get (oldEntries, newEntries) match {
reSync(pkg.moduleClass, allNested, oldNested, newNested, invalidated, failed) case (Some(oldcp) , Some(newcp)) =>
for (pstr <- packageNames(oldcp) ++ packageNames(newcp)) {
val pname = newTermName(pstr)
val pkg = (root.info decl pname) orElse {
// package was created by external agent, create symbol to track it
assert(!subPackage(oldcp, pstr).isDefined)
loaders.enterPackage(root, pstr, new loaders.PackageLoader(allEntries.get))
}
reSync(
pkg.moduleClass.asInstanceOf[ClassSymbol],
subPackage(allEntries.get, pstr), subPackage(oldcp, pstr), subPackage(newcp, pstr),
invalidated, failed)
}
case (Some(oldcp), None) =>
invalidateOrRemove(root)
case (None, Some(newcp)) =>
invalidateOrRemove(root)
case (None, None) =>
} }
} }
} }


/** Invalidate contents of setting -Yinvalidate */ /** Invalidate contents of setting -Yinvalidate */
def doInvalidation() = settings.Yinvalidate.value match { def doInvalidation() = settings.Yinvalidate.value match {
case "" => case "" =>
case entry => invalidateClassPathEntry(entry) case entry => invalidateClassPathEntries(entry)
} }


// ----------- Runs --------------------------------------- // ----------- Runs ---------------------------------------
Expand All @@ -953,7 +1021,7 @@ class Global(var currentSettings: Settings, var reporter: Reporter) extends Symb
* *
* @param sym A class symbol, object symbol, package, or package class. * @param sym A class symbol, object symbol, package, or package class.
*/ */
@deprecated("use invalidateClassPathEntry instead") @deprecated("use invalidateClassPathEntries instead")
def clearOnNextRun(sym: Symbol) = false def clearOnNextRun(sym: Symbol) = false
/* To try out clearOnNext run on the scala.tools.nsc project itself /* To try out clearOnNext run on the scala.tools.nsc project itself
* replace `false` above with the following code * replace `false` above with the following code
Expand Down Expand Up @@ -1210,7 +1278,7 @@ class Global(var currentSettings: Settings, var reporter: Reporter) extends Symb
/** Reset all classes contained in current project, as determined by /** Reset all classes contained in current project, as determined by
* the clearOnNextRun hook * the clearOnNextRun hook
*/ */
@deprecated("use invalidateClassPathEntry instead") @deprecated("use invalidateClassPathEntries instead")
def resetProjectClasses(root: Symbol): Unit = try { def resetProjectClasses(root: Symbol): Unit = try {
def unlink(sym: Symbol) = def unlink(sym: Symbol) =
if (sym != NoSymbol) root.info.decls.unlink(sym) if (sym != NoSymbol) root.info.decls.unlink(sym)
Expand Down Expand Up @@ -1443,6 +1511,7 @@ class Global(var currentSettings: Settings, var reporter: Reporter) extends Symb
def compileUnits(units: List[CompilationUnit], fromPhase: Phase) { def compileUnits(units: List[CompilationUnit], fromPhase: Phase) {
try compileUnitsInternal(units, fromPhase) try compileUnitsInternal(units, fromPhase)
catch { case ex => catch { case ex =>
// ex.printStackTrace(Console.out) // DEBUG for fsc, note that error stacktraces do not print in fsc
globalError(supplementErrorMessage("uncaught exception during compilation: " + ex.getClass.getName)) globalError(supplementErrorMessage("uncaught exception during compilation: " + ex.getClass.getName))
throw ex throw ex
} }
Expand Down
6 changes: 3 additions & 3 deletions src/compiler/scala/tools/nsc/backend/JavaPlatform.scala
Expand Up @@ -23,10 +23,10 @@ trait JavaPlatform extends Platform {
if (currentClassPath.isEmpty) currentClassPath = Some(new PathResolver(settings).result) if (currentClassPath.isEmpty) currentClassPath = Some(new PathResolver(settings).result)
currentClassPath.get currentClassPath.get
} }

/** Update classpath with a substituted subentry */ /** Update classpath with a substituted subentry */
def updateClassPath(oldEntry: ClassPath[BinaryRepr], newEntry: ClassPath[BinaryRepr]) = def updateClassPath(subst: Map[ClassPath[BinaryRepr], ClassPath[BinaryRepr]]) =
currentClassPath = Some(new DeltaClassPath(currentClassPath.get, oldEntry, newEntry)) currentClassPath = Some(new DeltaClassPath(currentClassPath.get, subst))


def rootLoader = new loaders.PackageLoader(classPath.asInstanceOf[ClassPath[platform.BinaryRepr]]) def rootLoader = new loaders.PackageLoader(classPath.asInstanceOf[ClassPath[platform.BinaryRepr]])
// [Martin] Why do we need a cast here? // [Martin] Why do we need a cast here?
Expand Down
6 changes: 3 additions & 3 deletions src/compiler/scala/tools/nsc/backend/MSILPlatform.scala
Expand Up @@ -30,11 +30,11 @@ trait MSILPlatform extends Platform {
lazy val classPath = MsilClassPath.fromSettings(settings) lazy val classPath = MsilClassPath.fromSettings(settings)
def rootLoader = new loaders.PackageLoader(classPath.asInstanceOf[ClassPath[platform.BinaryRepr]]) def rootLoader = new loaders.PackageLoader(classPath.asInstanceOf[ClassPath[platform.BinaryRepr]])
// See discussion in JavaPlatForm for why we need a cast here. // See discussion in JavaPlatForm for why we need a cast here.

/** Update classpath with a substituted subentry */ /** Update classpath with a substituted subentry */
def updateClassPath(oldEntry: ClassPath[BinaryRepr], newEntry: ClassPath[BinaryRepr]) = def updateClassPath(subst: Map[ClassPath[BinaryRepr], ClassPath[BinaryRepr]]) =
throw new UnsupportedOperationException("classpath invalidations not supported on MSIL") throw new UnsupportedOperationException("classpath invalidations not supported on MSIL")

def platformPhases = List( def platformPhases = List(
genMSIL // generate .msil files genMSIL // generate .msil files
) )
Expand Down
6 changes: 3 additions & 3 deletions src/compiler/scala/tools/nsc/backend/Platform.scala
Expand Up @@ -23,9 +23,9 @@ trait Platform {


/** The root symbol loader. */ /** The root symbol loader. */
def rootLoader: LazyType def rootLoader: LazyType

/** Update classpath with a substituted subentry */ /** Update classpath with a substitution that maps entries to entries */
def updateClassPath(oldEntry: ClassPath[BinaryRepr], newEntry: ClassPath[BinaryRepr]) def updateClassPath(subst: Map[ClassPath[BinaryRepr], ClassPath[BinaryRepr]])


/** Any platform-specific phases. */ /** Any platform-specific phases. */
def platformPhases: List[SubComponent] def platformPhases: List[SubComponent]
Expand Down
74 changes: 40 additions & 34 deletions src/compiler/scala/tools/nsc/symtab/SymbolLoaders.scala
Expand Up @@ -51,6 +51,46 @@ abstract class SymbolLoaders {
enterIfNew(owner, module, completer) enterIfNew(owner, module, completer)
} }


/** Enter package with given `name` into scope of `root`
* and give them `completer` as type.
*/
def enterPackage(root: Symbol, name: String, completer: SymbolLoader): Symbol = {
val pname = newTermName(name)
val preExisting = root.info.decls lookup pname
if (preExisting != NoSymbol) {
// Some jars (often, obfuscated ones) include a package and
// object with the same name. Rather than render them unusable,
// offer a setting to resolve the conflict one way or the other.
// This was motivated by the desire to use YourKit probes, which
// require yjp.jar at runtime. See SI-2089.
if (settings.termConflict.isDefault)
throw new TypeError(
root+" contains object and package with same name: "+
name+"\none of them needs to be removed from classpath"
)
else if (settings.termConflict.value == "package") {
global.warning(
"Resolving package/object name conflict in favor of package " +
preExisting.fullName + ". The object will be inaccessible."
)
root.info.decls.unlink(preExisting)
}
else {
global.warning(
"Resolving package/object name conflict in favor of object " +
preExisting.fullName + ". The package will be inaccessible."
)
return NoSymbol
}
}
// todo: find out initialization sequence for pkg/pkg.moduleClass is different from enterModule
val pkg = root.newPackage(pname)
pkg.moduleClass setInfo completer
pkg setInfo pkg.moduleClass.tpe
root.info.decls enter pkg
pkg
}

/** Enter class and module with given `name` into scope of `root` /** Enter class and module with given `name` into scope of `root`
* and give them `completer` as type. * and give them `completer` as type.
*/ */
Expand Down Expand Up @@ -171,40 +211,6 @@ abstract class SymbolLoaders {
class PackageLoader(classpath: ClassPath[platform.BinaryRepr]) extends SymbolLoader { class PackageLoader(classpath: ClassPath[platform.BinaryRepr]) extends SymbolLoader {
protected def description = "package loader "+ classpath.name protected def description = "package loader "+ classpath.name


def enterPackage(root: Symbol, name: String, completer: SymbolLoader) {
val preExisting = root.info.decls.lookup(newTermName(name))
if (preExisting != NoSymbol) {
// Some jars (often, obfuscated ones) include a package and
// object with the same name. Rather than render them unusable,
// offer a setting to resolve the conflict one way or the other.
// This was motivated by the desire to use YourKit probes, which
// require yjp.jar at runtime. See SI-2089.
if (settings.termConflict.isDefault)
throw new TypeError(
root+" contains object and package with same name: "+
name+"\none of them needs to be removed from classpath"
)
else if (settings.termConflict.value == "package") {
global.warning(
"Resolving package/object name conflict in favor of package " +
preExisting.fullName + ". The object will be inaccessible."
)
root.info.decls.unlink(preExisting)
}
else {
global.warning(
"Resolving package/object name conflict in favor of object " +
preExisting.fullName + ". The package will be inaccessible."
)
return
}
}
val pkg = root.newPackage(newTermName(name))
pkg.moduleClass.setInfo(completer)
pkg.setInfo(pkg.moduleClass.tpe)
root.info.decls.enter(pkg)
}

protected def doComplete(root: Symbol) { protected def doComplete(root: Symbol) {
assert(root.isPackageClass, root) assert(root.isPackageClass, root)
root.setInfo(new PackageClassInfoType(newScope, root)) root.setInfo(new PackageClassInfoType(newScope, root))
Expand Down
8 changes: 5 additions & 3 deletions src/compiler/scala/tools/nsc/util/ClassPath.scala
Expand Up @@ -312,9 +312,11 @@ class DirectoryClassPath(val dir: AbstractFile, val context: ClassPathContext[Ab
override def toString() = "directory classpath: "+ origin.getOrElse("?") override def toString() = "directory classpath: "+ origin.getOrElse("?")
} }


class DeltaClassPath[T](original: MergedClassPath[T], oldEntry: ClassPath[T], newEntry: ClassPath[T]) class DeltaClassPath[T](original: MergedClassPath[T], subst: Map[ClassPath[T], ClassPath[T]])
extends MergedClassPath[T](original.entries map (e => if (e == oldEntry) newEntry else e), original.context) { extends MergedClassPath[T](original.entries map (e => subst getOrElse (e, e)), original.context) {
require(original.entries contains oldEntry) // not sure we should require that here. Commented out for now.
// require(subst.keySet subsetOf original.entries.toSet)
// We might add specialized operations for computing classes packages here. Not sure it's worth it.
} }


/** /**
Expand Down

0 comments on commit e156d4a

Please sign in to comment.