Skip to content

Commit 65c7072

Browse files
Loading symbols from TASTy files directly (#17594)
Before this PR we used to parse the classfiles first and when we found a classfile that has a `TASTY` attribute we switched and loaded the tasty. Now we find the `.tasty` files directly and load the symbols directly from them. We still load the class files to check that the UUID in that classfile matches the UUID in the TASTy file. When looking for classes in the classpath, we prioritize the TASTy files over classfiles. This implies that the symbol loader will receive the `.tasty` files for Scala 3 code and `.class` for Scala 2 and Java code. A variant of the `ClassfileParser` called `ClassfileTastyUUIDParser` was added to have a way to check the UUID in the `TASTY` attribute of the classfile. The `ClassfileParser` could not be used directly because it eagerly tries to initialize parts of the symbols that are already loaded from the TASTy file, causing some conflicts. Open question: should we only check the TASTy UUID under some flag to avoid loading both the `.tasty` and the `.class` files? The second commit introduces this check.
2 parents b72dfb5 + f9e8b36 commit 65c7072

25 files changed

+499
-269
lines changed

compiler/src/dotty/tools/backend/jvm/PostProcessor.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ class PostProcessor(val frontendAccess: PostProcessorFrontendAccess, val bTypes:
2323

2424
def postProcessAndSendToDisk(generatedDefs: GeneratedDefs): Unit = {
2525
val GeneratedDefs(classes, tasty) = generatedDefs
26+
if !ctx.settings.YoutputOnlyTasty.value then
27+
postProcessClassesAndSendToDisk(classes)
28+
postProcessTastyAndSendToDisk(tasty)
29+
}
30+
31+
private def postProcessClassesAndSendToDisk(classes: List[GeneratedClass]): Unit = {
2632
for (GeneratedClass(classNode, sourceFile, isArtifact, onFileCreated) <- classes) {
2733
val bytes =
2834
try
@@ -46,8 +52,10 @@ class PostProcessor(val frontendAccess: PostProcessorFrontendAccess, val bTypes:
4652
if clsFile != null then onFileCreated(clsFile)
4753
}
4854
}
55+
}
4956

50-
for (GeneratedTasty(classNode, binaryGen) <- tasty){
57+
private def postProcessTastyAndSendToDisk(tasty: List[GeneratedTasty]): Unit = {
58+
for (GeneratedTasty(classNode, binaryGen) <- tasty) {
5159
classfileWriter.writeTasty(classNode.name.nn, binaryGen())
5260
}
5361
}

compiler/src/dotty/tools/dotc/classpath/DirectoryClassPath.scala

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,15 +278,17 @@ case class DirectoryClassPath(dir: JFile) extends JFileDirectoryLookup[ClassFile
278278

279279
def findClassFile(className: String): Option[AbstractFile] = {
280280
val relativePath = FileUtils.dirPath(className)
281-
val classFile = new JFile(dir, relativePath + ".class")
282-
if (classFile.exists) {
283-
Some(classFile.toPath.toPlainFile)
284-
}
285-
else None
281+
val tastyFile = new JFile(dir, relativePath + ".tasty")
282+
if tastyFile.exists then Some(tastyFile.toPath.toPlainFile)
283+
else
284+
val classFile = new JFile(dir, relativePath + ".class")
285+
if classFile.exists then Some(classFile.toPath.toPlainFile)
286+
else None
286287
}
287288

288289
protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
289-
protected def isMatchingFile(f: JFile): Boolean = f.isClass
290+
protected def isMatchingFile(f: JFile): Boolean =
291+
f.isTasty || (f.isClass && f.classToTasty.isEmpty)
290292

291293
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
292294
}

compiler/src/dotty/tools/dotc/classpath/FileUtils.scala

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ object FileUtils {
2020
def isClass: Boolean = !file.isDirectory && file.hasExtension("class") && !file.name.endsWith("$class.class")
2121
// FIXME: drop last condition when we stop being compatible with Scala 2.11
2222

23+
def isTasty: Boolean = !file.isDirectory && file.hasExtension("tasty")
24+
25+
def isScalaBinary: Boolean = file.isClass || file.isTasty
26+
2327
def isScalaOrJavaSource: Boolean = !file.isDirectory && (file.hasExtension("scala") || file.hasExtension("java"))
2428

2529
// TODO do we need to check also other files using ZipMagicNumber like in scala.tools.nsc.io.Jar.isJarOrZip?
@@ -30,17 +34,34 @@ object FileUtils {
3034
* and returning given default value in other case
3135
*/
3236
def toURLs(default: => Seq[URL] = Seq.empty): Seq[URL] = if (file.file == null) default else Seq(file.toURL)
37+
38+
/** Returns the tasty file associated with this class file */
39+
def classToTasty: Option[AbstractFile] =
40+
assert(file.isClass, s"non-class: $file")
41+
val tastyName = classNameToTasty(file.name)
42+
Option(file.resolveSibling(tastyName))
3343
}
3444

3545
extension (file: JFile) {
3646
def isPackage: Boolean = file.isDirectory && mayBeValidPackage(file.getName)
3747

38-
def isClass: Boolean = file.isFile && file.getName.endsWith(".class") && !file.getName.endsWith("$class.class")
39-
// FIXME: drop last condition when we stop being compatible with Scala 2.11
48+
def isClass: Boolean = file.isFile && file.getName.endsWith(SUFFIX_CLASS) && !file.getName.endsWith("$class.class")
49+
// FIXME: drop last condition when we stop being compatible with Scala 2.11
50+
51+
def isTasty: Boolean = file.isFile && file.getName.endsWith(SUFFIX_TASTY)
52+
53+
/** Returns the tasty file associated with this class file */
54+
def classToTasty: Option[JFile] =
55+
assert(file.isClass, s"non-class: $file")
56+
val tastyName = classNameToTasty(file.getName.stripSuffix(".class"))
57+
val tastyPath = file.toPath.resolveSibling(tastyName)
58+
if java.nio.file.Files.exists(tastyPath) then Some(tastyPath.toFile) else None
59+
4060
}
4161

4262
private val SUFFIX_CLASS = ".class"
4363
private val SUFFIX_SCALA = ".scala"
64+
private val SUFFIX_TASTY = ".tasty"
4465
private val SUFFIX_JAVA = ".java"
4566
private val SUFFIX_SIG = ".sig"
4667

@@ -81,4 +102,15 @@ object FileUtils {
81102
def mkFileFilter(f: JFile => Boolean): FileFilter = new FileFilter {
82103
def accept(pathname: JFile): Boolean = f(pathname)
83104
}
105+
106+
/** Transforms a .class file name to a .tasty file name */
107+
private def classNameToTasty(fileName: String): String =
108+
val classOrModuleName = fileName.stripSuffix(".class")
109+
val className =
110+
if classOrModuleName.endsWith("$")
111+
&& classOrModuleName != "Null$" // scala.runtime.Null$
112+
&& classOrModuleName != "Nothing$" // scala.runtime.Nothing$
113+
then classOrModuleName.stripSuffix("$")
114+
else classOrModuleName
115+
className + SUFFIX_TASTY
84116
}

compiler/src/dotty/tools/dotc/classpath/VirtualDirectoryClassPath.scala

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,17 @@ case class VirtualDirectoryClassPath(dir: VirtualDirectory) extends ClassPath wi
4141
override def findClass(className: String): Option[ClassRepresentation] = findClassFile(className) map ClassFileEntryImpl.apply
4242

4343
def findClassFile(className: String): Option[AbstractFile] = {
44-
val relativePath = FileUtils.dirPath(className) + ".class"
45-
Option(lookupPath(dir)(relativePath.split(java.io.File.separator).toIndexedSeq, directory = false))
44+
val pathSeq = FileUtils.dirPath(className).split(java.io.File.separator)
45+
val parentDir = lookupPath(dir)(pathSeq.init.toSeq, directory = true)
46+
if parentDir == null then return None
47+
else
48+
Option(lookupPath(parentDir)(pathSeq.last + ".tasty" :: Nil, directory = false))
49+
.orElse(Option(lookupPath(parentDir)(pathSeq.last + ".class" :: Nil, directory = false)))
4650
}
4751

4852
private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
4953

5054
protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
51-
protected def isMatchingFile(f: AbstractFile): Boolean = f.isClass
55+
protected def isMatchingFile(f: AbstractFile): Boolean =
56+
f.isTasty || (f.isClass && f.classToTasty.isEmpty)
5257
}

compiler/src/dotty/tools/dotc/classpath/ZipAndJarFileLookupFactory.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,21 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
4444
extends ZipArchiveFileLookup[ClassFileEntryImpl]
4545
with NoSourcePaths {
4646

47-
override def findClassFile(className: String): Option[AbstractFile] = {
48-
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
49-
file(PackageName(pkg), simpleClassName + ".class").map(_.file)
50-
}
47+
override def findClassFile(className: String): Option[AbstractFile] =
48+
findClass(className).map(_.file)
5149

5250
// This method is performance sensitive as it is used by SBT's ExtractDependencies phase.
53-
override def findClass(className: String): Option[ClassRepresentation] = {
51+
override def findClass(className: String): Option[ClassFileEntryImpl] = {
5452
val (pkg, simpleClassName) = PackageNameUtils.separatePkgAndClassNames(className)
55-
file(PackageName(pkg), simpleClassName + ".class")
53+
val binaries = files(PackageName(pkg), simpleClassName + ".tasty", simpleClassName + ".class")
54+
binaries.find(_.file.isTasty).orElse(binaries.find(_.file.isClass))
5655
}
5756

5857
override private[dotty] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
5958

6059
override protected def createFileEntry(file: FileZipArchive#Entry): ClassFileEntryImpl = ClassFileEntryImpl(file)
61-
override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isClass
60+
override protected def isRequiredFileType(file: AbstractFile): Boolean =
61+
file.isTasty || (file.isClass && file.classToTasty.isEmpty)
6262
}
6363

6464
/**

compiler/src/dotty/tools/dotc/classpath/ZipArchiveFileLookup.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ trait ZipArchiveFileLookup[FileEntryType <: ClassRepresentation] extends Efficie
4343
}
4444
yield createFileEntry(entry)
4545

46+
protected def files(inPackage: PackageName, names: String*): Seq[FileEntryType] =
47+
for {
48+
dirEntry <- findDirEntry(inPackage).toSeq
49+
name <- names
50+
entry <- Option(dirEntry.lookupName(name, directory = false))
51+
if isRequiredFileType(entry)
52+
}
53+
yield createFileEntry(entry)
54+
4655
protected def file(inPackage: PackageName, name: String): Option[FileEntryType] =
4756
for {
4857
dirEntry <- findDirEntry(inPackage)

compiler/src/dotty/tools/dotc/config/JavaPlatform.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,7 @@ class JavaPlatform extends Platform {
6666

6767
def newClassLoader(bin: AbstractFile)(using Context): SymbolLoader =
6868
new ClassfileLoader(bin)
69+
70+
def newTastyLoader(bin: AbstractFile)(using Context): SymbolLoader =
71+
new TastyLoader(bin)
6972
}

compiler/src/dotty/tools/dotc/config/Platform.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ abstract class Platform {
3636
/** Create a new class loader to load class file `bin` */
3737
def newClassLoader(bin: AbstractFile)(using Context): SymbolLoader
3838

39+
/** Create a new TASTy loader to load class file `bin` */
40+
def newTastyLoader(bin: AbstractFile)(using Context): SymbolLoader
41+
3942
/** The given symbol is a method with the right name and signature to be a runnable program. */
4043
def isMainMethod(sym: Symbol)(using Context): Boolean
4144

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ private sealed trait YSettings:
369369
val YnoExperimental: Setting[Boolean] = BooleanSetting("-Yno-experimental", "Disable experimental language features.")
370370
val YlegacyLazyVals: Setting[Boolean] = BooleanSetting("-Ylegacy-lazy-vals", "Use legacy (pre 3.3.0) implementation of lazy vals.")
371371
val Yscala2Stdlib: Setting[Boolean] = BooleanSetting("-Yscala2-stdlib", "Used when compiling the Scala 2 standard library.")
372+
val YoutputOnlyTasty: Setting[Boolean] = BooleanSetting("-Youtput-only-tasty", "Used to only generate the TASTy file without the classfiles")
372373

373374
val YprofileEnabled: Setting[Boolean] = BooleanSetting("-Yprofile-enabled", "Enable profiling.")
374375
val YprofileDestination: Setting[String] = StringSetting("-Yprofile-destination", "file", "Where to send profiling output - specify a file, default is to the console.", "")

compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import java.nio.channels.ClosedByInterruptException
77

88
import scala.util.control.NonFatal
99

10+
import dotty.tools.dotc.classpath.FileUtils.isTasty
1011
import dotty.tools.io.{ ClassPath, ClassRepresentation, AbstractFile }
1112
import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions
1213

1314
import Contexts._, Symbols._, Flags._, SymDenotations._, Types._, Scopes._, Names._
1415
import NameOps._
1516
import StdNames._
16-
import classfile.ClassfileParser
17+
import classfile.{ClassfileParser, ClassfileTastyUUIDParser}
1718
import Decorators._
1819

1920
import util.Stats
@@ -23,6 +24,7 @@ import ast.desugar
2324

2425
import parsing.JavaParsers.OutlineJavaParser
2526
import parsing.Parsers.OutlineParser
27+
import dotty.tools.tasty.TastyHeaderUnpickler
2628

2729

2830
object SymbolLoaders {
@@ -192,10 +194,13 @@ object SymbolLoaders {
192194
if (ctx.settings.verbose.value) report.inform("[symloader] picked up newer source file for " + src.path)
193195
enterToplevelsFromSource(owner, nameOf(classRep), src)
194196
case (None, Some(src)) =>
195-
if (ctx.settings.verbose.value) report.inform("[symloader] no class, picked up source file for " + src.path)
197+
if (ctx.settings.verbose.value) report.inform("[symloader] no class or tasty, picked up source file for " + src.path)
196198
enterToplevelsFromSource(owner, nameOf(classRep), src)
197199
case (Some(bin), _) =>
198-
enterClassAndModule(owner, nameOf(classRep), ctx.platform.newClassLoader(bin))
200+
val completer =
201+
if bin.isTasty then ctx.platform.newTastyLoader(bin)
202+
else ctx.platform.newClassLoader(bin)
203+
enterClassAndModule(owner, nameOf(classRep), completer)
199204
}
200205

201206
def needCompile(bin: AbstractFile, src: AbstractFile): Boolean =
@@ -404,20 +409,38 @@ class ClassfileLoader(val classfile: AbstractFile) extends SymbolLoader {
404409
def description(using Context): String = "class file " + classfile.toString
405410

406411
override def doComplete(root: SymDenotation)(using Context): Unit =
407-
load(root)
408-
409-
def load(root: SymDenotation)(using Context): Unit = {
410412
val (classRoot, moduleRoot) = rootDenots(root.asClass)
411413
val classfileParser = new ClassfileParser(classfile, classRoot, moduleRoot)(ctx)
412-
val result = classfileParser.run()
413-
if (mayLoadTreesFromTasty)
414-
result match {
415-
case Some(unpickler: tasty.DottyUnpickler) =>
416-
classRoot.classSymbol.rootTreeOrProvider = unpickler
417-
moduleRoot.classSymbol.rootTreeOrProvider = unpickler
418-
case _ =>
419-
}
420-
}
414+
classfileParser.run()
415+
}
416+
417+
class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
418+
419+
override def sourceFileOrNull: AbstractFile | Null = tastyFile
420+
421+
def description(using Context): String = "TASTy file " + tastyFile.toString
422+
423+
override def doComplete(root: SymDenotation)(using Context): Unit =
424+
val (classRoot, moduleRoot) = rootDenots(root.asClass)
425+
val tastyBytes = tastyFile.toByteArray
426+
val unpickler = new tasty.DottyUnpickler(tastyBytes)
427+
unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource))
428+
if mayLoadTreesFromTasty then
429+
classRoot.classSymbol.rootTreeOrProvider = unpickler
430+
moduleRoot.classSymbol.rootTreeOrProvider = unpickler
431+
checkTastyUUID(tastyFile, tastyBytes)
432+
433+
434+
private def checkTastyUUID(tastyFile: AbstractFile, tastyBytes: Array[Byte])(using Context): Unit =
435+
var classfile = tastyFile.resolveSibling(tastyFile.name.stripSuffix(".tasty") + ".class")
436+
if classfile == null then
437+
classfile = tastyFile.resolveSibling(tastyFile.name.stripSuffix(".tasty") + "$.class")
438+
if classfile != null then
439+
val tastyUUID = new TastyHeaderUnpickler(tastyBytes).readHeader()
440+
new ClassfileTastyUUIDParser(classfile)(ctx).checkTastyUUID(tastyUUID)
441+
else
442+
// This will be the case in any of our tests that compile with `-Youtput-only-tasty`
443+
report.inform(s"No classfiles found for $tastyFile when checking TASTy UUID")
421444

422445
private def mayLoadTreesFromTasty(using Context): Boolean =
423446
ctx.settings.YretainTrees.value || ctx.settings.fromTasty.value

compiler/src/dotty/tools/dotc/core/Symbols.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import io.AbstractFile
3131
import util.{SourceFile, NoSource, Property, SourcePosition, SrcPos, EqHashMap}
3232
import scala.annotation.internal.sharable
3333
import config.Printers.typr
34+
import dotty.tools.dotc.classpath.FileUtils.isScalaBinary
3435

3536
object Symbols {
3637

@@ -151,7 +152,7 @@ object Symbols {
151152
* symbols defined by the user in a prior run of the REPL, that are still valid.
152153
*/
153154
final def isDefinedInSource(using Context): Boolean =
154-
span.exists && isValidInCurrentRun && associatedFileMatches(_.extension != "class")
155+
span.exists && isValidInCurrentRun && associatedFileMatches(!_.isScalaBinary)
155156

156157
/** Is symbol valid in current run? */
157158
final def isValidInCurrentRun(using Context): Boolean =
@@ -272,7 +273,7 @@ object Symbols {
272273
/** The class file from which this class was generated, null if not applicable. */
273274
final def binaryFile(using Context): AbstractFile | Null = {
274275
val file = associatedFile
275-
if (file != null && file.extension == "class") file else null
276+
if file != null && file.isScalaBinary then file else null
276277
}
277278

278279
/** A trap to avoid calling x.symbol on something that is already a symbol.
@@ -285,7 +286,7 @@ object Symbols {
285286

286287
final def source(using Context): SourceFile = {
287288
def valid(src: SourceFile): SourceFile =
288-
if (src.exists && src.file.extension != "class") src
289+
if (src.exists && !src.file.isScalaBinary) src
289290
else NoSource
290291

291292
if (!denot.exists) NoSource
@@ -463,7 +464,7 @@ object Symbols {
463464
if !mySource.exists && !denot.is(Package) then
464465
// this allows sources to be added in annotations after `sourceOfClass` is first called
465466
val file = associatedFile
466-
if file != null && file.extension != "class" then
467+
if file != null && !file.isScalaBinary then
467468
mySource = ctx.getSource(file)
468469
else
469470
mySource = defn.patchSource(this)

0 commit comments

Comments
 (0)