Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TASTy Reader: support Scala 3.4 [ci: last-only] #10670

Merged
merged 19 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion project/DottySupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import sbt.librarymanagement.{
* Settings to support validation of TastyUnpickler against the release of dotty with the matching TASTy version
*/
object TastySupport {
val supportedTASTyRelease = "3.3.1"
val supportedTASTyRelease = "3.4.0-RC4" // TASTY: 28.4-experimental-1 (preparing for final release 28.4)
val scala3Compiler = "org.scala-lang" % "scala3-compiler_3" % supportedTASTyRelease
val scala3Library = "org.scala-lang" % "scala3-library_3" % supportedTASTyRelease

Expand Down
16 changes: 11 additions & 5 deletions src/compiler/scala/tools/nsc/classpath/DirectoryClassPath.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ trait DirectoryLookup[FileEntryType <: ClassRepresentation] extends EfficientCla
protected def emptyFiles: Array[F] // avoids reifying ClassTag[F]
protected def getSubDir(dirName: String): Option[F]
protected def listChildren(dir: F, filter: Option[F => Boolean] = None): Array[F]
protected def hasChild(dir: F, name: String): Boolean
protected def getName(f: F): String
protected def toAbstractFile(f: F): AbstractFile
protected def isPackage(f: F): Boolean

protected def createFileEntry(file: AbstractFile): FileEntryType
protected def isMatchingFile(f: F): Boolean
protected def isMatchingFile(f: F, siblingExists: String => Boolean): Boolean

private def getDirectory(forPackage: PackageName): Option[F] = {
if (forPackage.isRoot) {
Expand All @@ -70,7 +71,9 @@ trait DirectoryLookup[FileEntryType <: ClassRepresentation] extends EfficientCla
val dirForPackage = getDirectory(inPackage)
val files: Array[F] = dirForPackage match {
case None => emptyFiles
case Some(directory) => listChildren(directory, Some(isMatchingFile))
case Some(directory) =>
val hasCh = hasChild(directory, _)
listChildren(directory, Some(f => isMatchingFile(f, hasCh)))
}
files.iterator.map(f => createFileEntry(toAbstractFile(f))).toSeq
}
Expand All @@ -80,10 +83,11 @@ trait DirectoryLookup[FileEntryType <: ClassRepresentation] extends EfficientCla
dirForPackage match {
case None =>
case Some(directory) =>
val hasCh = hasChild(directory, _)
for (file <- listChildren(directory)) {
if (isPackage(file))
onPackageEntry(PackageEntryImpl(inPackage.entryName(getName(file))))
else if (isMatchingFile(file))
else if (isMatchingFile(file, hasCh))
onClassesAndSources(createFileEntry(toAbstractFile(file)))
}
}
Expand Down Expand Up @@ -118,6 +122,7 @@ trait JFileDirectoryLookup[FileEntryType <: ClassRepresentation] extends Directo
java.util.Arrays.sort(listing, (o1: File, o2: File) => o1.getName.compareTo(o2.getName))
listing
}
protected def hasChild(dir: File, name: String): Boolean = new File(dir, name).isFile
protected def getName(f: File): String = f.getName
protected def toAbstractFile(f: File): AbstractFile = new PlainFile(new scala.reflect.io.File(f))
protected def isPackage(f: File): Boolean = f.isPackage
Expand Down Expand Up @@ -304,7 +309,8 @@ case class DirectoryClassPath(dir: File) extends JFileDirectoryLookup[ClassFileE
}

protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
protected def isMatchingFile(f: File): Boolean = f.isClass
protected def isMatchingFile(f: File, siblingExists: String => Boolean): Boolean =
f.isClass && !(f.getName.endsWith(".class") && siblingExists(f.getName.dropRight(6) + ".tasty"))

private[nsc] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)
}
Expand All @@ -313,7 +319,7 @@ case class DirectorySourcePath(dir: File) extends JFileDirectoryLookup[SourceFil
def asSourcePathString: String = asClassPathString

protected def createFileEntry(file: AbstractFile): SourceFileEntryImpl = SourceFileEntryImpl(file)
protected def isMatchingFile(f: File): Boolean = endsScalaOrJava(f.getName)
protected def isMatchingFile(f: File, siblingExists: String => Boolean): Boolean = endsScalaOrJava(f.getName)

override def findClass(className: String): Option[ClassRepresentation] = findSourceFile(className) map SourceFileEntryImpl

Expand Down
5 changes: 3 additions & 2 deletions src/compiler/scala/tools/nsc/classpath/FileUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ object FileUtils {
implicit class AbstractFileOps(val file: AbstractFile) extends AnyVal {
def isPackage: Boolean = file.isDirectory && mayBeValidPackage(file.name)

def isClass: Boolean = !file.isDirectory && (file.hasExtension("class") || file.hasExtension("sig"))
def isClass: Boolean = !file.isDirectory && (file.hasExtension("class") || file.hasExtension("sig") || file.hasExtension("tasty"))
bishabosha marked this conversation as resolved.
Show resolved Hide resolved

def isScalaOrJavaSource: Boolean = !file.isDirectory && (file.hasExtension("scala") || file.hasExtension("java"))

Expand All @@ -46,6 +46,7 @@ object FileUtils {
private val SUFFIX_SCALA = ".scala"
private val SUFFIX_JAVA = ".java"
private val SUFFIX_SIG = ".sig"
private val SUFFIX_TASTY = ".tasty"

def stripSourceExtension(fileName: String): String = {
if (endsScala(fileName)) stripClassExtension(fileName)
Expand All @@ -58,7 +59,7 @@ object FileUtils {
@inline private def ends (filename:String, suffix:String) = filename.endsWith(suffix) && filename.length > suffix.length

def endsClass(fileName: String): Boolean =
ends (fileName, SUFFIX_CLASS) || fileName.endsWith(SUFFIX_SIG)
ends (fileName, SUFFIX_CLASS) || fileName.endsWith(SUFFIX_SIG) || fileName.endsWith(SUFFIX_TASTY)

def endsScalaOrJava(fileName: String): Boolean =
endsScala(fileName) || endsJava(fileName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ case class VirtualDirectoryClassPath(dir: VirtualDirectory) extends ClassPath wi
case Some(f) => dir.iterator.filter(f).toArray
case _ => dir.toArray
}
protected def hasChild(dir: AbstractFile, name: String): Boolean = dir.lookupName(name, directory = false) != null

def getName(f: AbstractFile): String = f.name
def toAbstractFile(f: AbstractFile): AbstractFile = f
def isPackage(f: AbstractFile): Boolean = f.isPackage
Expand All @@ -47,5 +49,6 @@ case class VirtualDirectoryClassPath(dir: VirtualDirectory) extends ClassPath wi
private[nsc] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)

protected def createFileEntry(file: AbstractFile): ClassFileEntryImpl = ClassFileEntryImpl(file)
protected def isMatchingFile(f: AbstractFile): Boolean = f.isClass
protected def isMatchingFile(f: AbstractFile, siblingExists: String => Boolean): Boolean =
f.isClass && !(f.hasExtension("class") && siblingExists(f.name.dropRight(6) + ".tasty"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ object ZipAndJarClassPathFactory extends ZipAndJarFileLookupFactory {
override private[nsc] def classes(inPackage: PackageName): Seq[ClassFileEntry] = files(inPackage)

override protected def createFileEntry(file: FileZipArchive#Entry): ClassFileEntryImpl = ClassFileEntryImpl(file)
override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isClass
override protected def isRequiredFileType(file: AbstractFile, siblingExists: String => Boolean): Boolean = {
file.isClass && !(file.hasExtension("class") && siblingExists(file.name.dropRight(6) + ".tasty"))
}
}

/**
Expand Down Expand Up @@ -182,7 +184,7 @@ object ZipAndJarSourcePathFactory extends ZipAndJarFileLookupFactory {
override private[nsc] def sources(inPackage: PackageName): Seq[SourceFileEntry] = files(inPackage)

override protected def createFileEntry(file: FileZipArchive#Entry): SourceFileEntryImpl = SourceFileEntryImpl(file)
override protected def isRequiredFileType(file: AbstractFile): Boolean = file.isScalaOrJavaSource
override protected def isRequiredFileType(file: AbstractFile, siblingExists: String => Boolean): Boolean = file.isScalaOrJavaSource
}

override protected def createForZipFile(zipFile: AbstractFile, zipSettings: ZipSettings): ClassPath with Closeable = ZipArchiveSourcePath(zipFile.file)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ trait ZipArchiveFileLookup[FileEntryType <: ClassRepresentation] extends Efficie
protected def files(inPackage: PackageName): Seq[FileEntryType] =
for {
dirEntry <- findDirEntry(inPackage).toSeq
entry <- dirEntry.iterator if isRequiredFileType(entry)
entry <- dirEntry.iterator if isRequiredFileType(entry, dirEntry.entries.contains)
} yield createFileEntry(entry)

protected def file(inPackage: PackageName, name: String): Option[FileEntryType] =
findDirEntry(inPackage) match {
case Some(dirEntry) =>
val entry = dirEntry.lookupName(name, directory = false)
if (entry != null && isRequiredFileType(entry))
if (entry != null)
Some(createFileEntry(entry))
else
None
Expand All @@ -68,7 +68,7 @@ trait ZipArchiveFileLookup[FileEntryType <: ClassRepresentation] extends Efficie
for (entry <- dirEntry.iterator) {
if (entry.isPackage)
onPackageEntry(PackageEntryImpl(inPackage.entryName(entry.name)))
else if (isRequiredFileType(entry))
else if (isRequiredFileType(entry, dirEntry.entries.contains))
onClassesAndSources(createFileEntry(entry))
}
case None =>
Expand All @@ -81,6 +81,6 @@ trait ZipArchiveFileLookup[FileEntryType <: ClassRepresentation] extends Efficie


protected def createFileEntry(file: FileZipArchive#Entry): FileEntryType
protected def isRequiredFileType(file: AbstractFile): Boolean
protected def isRequiredFileType(file: AbstractFile, siblingExists: String => Boolean): Boolean
}

101 changes: 17 additions & 84 deletions src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,19 @@ package tools.nsc
package symtab
package classfile

import java.io.{ByteArrayOutputStream, IOException}
import java.io.IOException
import java.lang.Integer.toHexString
import java.net.URLClassLoader
import java.util.UUID

import scala.annotation.switch
import scala.collection.{immutable, mutable}, mutable.{ArrayBuffer, ListBuffer}
import scala.reflect.internal.JavaAccFlags
import scala.reflect.internal.pickling.ByteCodecs
import scala.reflect.internal.util.ReusableInstance
import scala.reflect.io.{NoAbstractFile, PlainFile, ZipArchive}
import scala.reflect.io.NoAbstractFile
import scala.tools.nsc.Reporting.WarningCategory
import scala.tools.nsc.io.AbstractFile
import scala.tools.nsc.util.ClassPath
import scala.tools.nsc.tasty.{TastyUniverse, TastyUnpickler}
import scala.tools.tasty.{TastyHeaderUnpickler, TastyReader}
import scala.util.control.NonFatal

/** This abstract class implements a class file parser.
Expand Down Expand Up @@ -73,14 +70,12 @@ abstract class ClassfileParser(reader: ReusableInstance[ReusableDataReader]) {
protected var staticScope: Scope = _ // the scope of all static definitions
protected var pool: ConstantPool = _ // the classfile's constant pool
protected var isScala: Boolean = _ // does class file describe a scala class?
protected var isTASTY: Boolean = _ // is this class accompanied by a TASTY file?
bishabosha marked this conversation as resolved.
Show resolved Hide resolved
protected var isScalaRaw: Boolean = _ // this class file is a scala class with no pickled info
protected var busy: Symbol = _ // lock to detect recursive reads
protected var currentClass: String = _ // JVM name of the current class
protected var classTParams = Map[Name,Symbol]()
protected var srcfile0 : Option[AbstractFile] = None
protected def moduleClass: Symbol = staticModule.moduleClass
protected val TASTYUUIDLength: Int = 16
private var YtastyReader = false

private def ownerForFlags(jflags: JavaAccFlags) = if (jflags.isStatic) moduleClass else clazz
Expand Down Expand Up @@ -163,11 +158,22 @@ abstract class ClassfileParser(reader: ReusableInstance[ReusableDataReader]) {
this.isScala = false
this.YtastyReader = settings.YtastyReader.value

val magic = in.getInt(in.bp)
if (magic != JAVA_MAGIC && file.name.endsWith(".sig")) {
val isJavaMagic = in.getInt(in.bp) == JAVA_MAGIC
if (!isJavaMagic && file.name.endsWith(".sig")) {
currentClass = clazz.javaClassName
isScala = true
unpickler.unpickle(in.buf.take(file.sizeOption.get), 0, clazz, staticModule, file.name)
} else if (!isJavaMagic && file.name.endsWith(".tasty")) {
if (!YtastyReader)
MissingRequirementError.signal(s"Add -Ytasty-reader to scalac options to parse the TASTy in $file")

// TODO [tasty]: it seems tests don't fail if we remove this, but previously this
// was added for the following reason:
// > Force scala.AnyRef, otherwise we get "error: Symbol AnyRef is missing from the classpath"
AnyRefClass

val bytes = in.buf.take(file.sizeOption.get)
TastyUnpickler.unpickle(TastyUniverse)(bytes, clazz, staticModule, file.path.stripSuffix(".class") + ".tasty")
} else {
parseHeader()
this.pool = new ConstantPool
Expand Down Expand Up @@ -501,7 +507,7 @@ abstract class ClassfileParser(reader: ReusableInstance[ReusableDataReader]) {
if (!c.isInstanceOf[StubSymbol] && c != clazz) mismatchError(c)
}

if (isScala || isTASTY) {
if (isScala) {
() // We're done
} else if (isScalaRaw) {
val decls = clazz.enclosingPackage.info.decls
Expand Down Expand Up @@ -1095,8 +1101,6 @@ abstract class ClassfileParser(reader: ReusableInstance[ReusableDataReader]) {

var innersStart = -1
var runtimeAnnotStart = -1
var TASTYAttrStart = -1
var TASTYAttrLen = -1

val numAttrs = u2()
var i = 0
Expand All @@ -1110,13 +1114,8 @@ abstract class ClassfileParser(reader: ReusableInstance[ReusableDataReader]) {
case tpnme.ScalaATTR =>
isScalaRaw = true
i = numAttrs
case tpnme.TASTYATTR if !YtastyReader =>
MissingRequirementError.signal(s"Add -Ytasty-reader to scalac options to parse the TASTy in $file")
case tpnme.TASTYATTR =>
isTASTY = true
TASTYAttrLen = attrLen
TASTYAttrStart = in.bp
i = numAttrs
MissingRequirementError.notFound(s"TASTy file for associated class file $file")
case tpnme.InnerClassesATTR =>
innersStart = in.bp
case tpnme.RuntimeAnnotationATTR =>
Expand All @@ -1128,13 +1127,6 @@ abstract class ClassfileParser(reader: ReusableInstance[ReusableDataReader]) {
i += 1
}

// To understand the situation, it's helpful to know that:
// - Scalac emits the `ScalaSignature` attribute for classfiles with pickled information
// and the `Scala` attribute for everything else.
// - Dotty emits the `TASTY` attribute for classfiles with pickled information
// and the `Scala` attribute for _every_ classfile.
isScalaRaw &= !isTASTY

if (isScala) {
def parseScalaSigBytes(): Array[Byte] = {
val tag = u1()
Expand Down Expand Up @@ -1212,65 +1204,6 @@ abstract class ClassfileParser(reader: ReusableInstance[ReusableDataReader]) {
AnyRefClass // Force scala.AnyRef, otherwise we get "error: Symbol AnyRef is missing from the classpath"
assert(bytes != null, s"No Scala(Long)Signature annotation in classfile with ScalaSignature attribute: $clazz")
unpickler.unpickle(bytes, 0, clazz, staticModule, file.name)
} else if (isTASTY) {

def parseTASTYFile(): Array[Byte] = file.underlyingSource match { // TODO: simplify when #3552 is fixed
case None =>
reporter.error(NoPosition, "Could not load TASTY from .tasty for virtual file " + file)
Array.empty
case Some(jar: ZipArchive) => // We are in a jar
val cl = new URLClassLoader(Array(jar.toURL), /*parent =*/ null)
val path = file.path.stripSuffix(".class") + ".tasty"
val stream = cl.getResourceAsStream(path)
if (stream != null) {
val tastyOutStream = new ByteArrayOutputStream()
val buffer = new Array[Byte](1024)
var read = stream.read(buffer, 0, buffer.length)
while (read != -1) {
tastyOutStream.write(buffer, 0, read)
read = stream.read(buffer, 0, buffer.length)
}
tastyOutStream.flush()
tastyOutStream.toByteArray
} else {
reporter.error(NoPosition, s"Could not find $path in $jar")
Array.empty
}
case _ =>
val plainFile = new PlainFile(io.File(file.path).changeExtension("tasty"))
if (plainFile.exists) plainFile.toByteArray
else {
reporter.error(NoPosition, "Could not find " + plainFile)
Array.empty
}
}

def parseTASTYBytes(): Array[Byte] = {
assert(TASTYAttrLen == TASTYUUIDLength, "TASTY Attribute is not a UUID")
assert(TASTYAttrStart != -1, "no TASTY Annotation position")
in.bp = TASTYAttrStart
val TASTY = in.nextBytes(TASTYUUIDLength)
val TASTYBytes = parseTASTYFile()
if (TASTYBytes.isEmpty) {
reporter.error(NoPosition, s"No Tasty file found for classfile $file with TASTY Attribute")
}
val reader = new TastyReader(TASTY, 0, TASTYUUIDLength)
val expectedUUID = new UUID(reader.readUncompressedLong(), reader.readUncompressedLong())
val tastyUUID = new TastyHeaderUnpickler(TASTYBytes).readHeader()
if (expectedUUID != tastyUUID) {
loaders.warning(
NoPosition,
s"$file is out of sync with its TASTy file. Loaded TASTy file. Try cleaning the project to fix this issue",
WarningCategory.Other,
clazz.fullNameString
)
}
TASTYBytes
}

AnyRefClass // Force scala.AnyRef, otherwise we get "error: Symbol AnyRef is missing from the classpath"
val bytes = parseTASTYBytes()
TastyUnpickler.unpickle(TastyUniverse)(bytes, clazz, staticModule, file.path.stripSuffix(".class") + ".tasty")
} else if (!isScalaRaw && innersStart != -1) {
in.bp = innersStart
val entries = u2()
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/scala/tools/nsc/tasty/TastyModes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ object TastyModes {
final val OpaqueTypeDef: TastyMode = TastyMode(1 << 6)
/** When reading trees of an annotation */
final val ReadAnnotationCtor: TastyMode = TastyMode(1 << 7)
/** When reading a TASTy file produced from a Java source file (file has JAVAattr attribute) */
final val ReadJava: TastyMode = TastyMode(1 << 8)

/** The union of `IndexStats` and `InnerScope` */
final val IndexScopedStats: TastyMode = IndexStats | InnerScope
Expand Down Expand Up @@ -63,6 +65,7 @@ object TastyModes {
if (mode.is(InnerScope)) sb += "InnerScope"
if (mode.is(OpaqueTypeDef)) sb += "OpaqueTypeDef"
if (mode.is(ReadAnnotationCtor)) sb += "ReadAnnotationCtor"
if (mode.is(ReadJava)) sb += "ReadJava"
sb.mkString(" | ")
}
}
Expand Down