diff --git a/compiler/src/dotty/tools/backend/jvm/CodeGen.scala b/compiler/src/dotty/tools/backend/jvm/CodeGen.scala index 9572777095e0..4bf305f3387c 100644 --- a/compiler/src/dotty/tools/backend/jvm/CodeGen.scala +++ b/compiler/src/dotty/tools/backend/jvm/CodeGen.scala @@ -25,6 +25,7 @@ import java.io.DataOutputStream import java.nio.channels.ClosedByInterruptException import dotty.tools.tasty.{ TastyBuffer, TastyHeaderUnpickler } +import dotty.tools.dotc.core.tasty.TastyUnpickler import scala.tools.asm import scala.tools.asm.tree._ @@ -94,7 +95,7 @@ class CodeGen(val int: DottyBackendInterface, val primitives: DottyPrimitives)( for (binary <- unit.pickled.get(claszSymbol.asClass)) { generatedTasty += GeneratedTasty(store, binary) val tasty = - val uuid = new TastyHeaderUnpickler(binary()).readHeader() + val uuid = new TastyHeaderUnpickler(TastyUnpickler.scala3CompilerConfig, binary()).readHeader() val lo = uuid.getMostSignificantBits val hi = uuid.getLeastSignificantBits diff --git a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala index 3969a09a69ee..12eea3a26df4 100644 --- a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala +++ b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala @@ -24,7 +24,8 @@ import ast.desugar import parsing.JavaParsers.OutlineJavaParser import parsing.Parsers.OutlineParser -import dotty.tools.tasty.TastyHeaderUnpickler +import dotty.tools.tasty.{TastyHeaderUnpickler, UnpickleException, UnpicklerConfig} +import dotty.tools.dotc.core.tasty.TastyUnpickler object SymbolLoaders { @@ -421,14 +422,25 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader { def description(using Context): String = "TASTy file " + tastyFile.toString override def doComplete(root: SymDenotation)(using Context): Unit = - val (classRoot, moduleRoot) = rootDenots(root.asClass) - val tastyBytes = tastyFile.toByteArray - val unpickler = new tasty.DottyUnpickler(tastyBytes) - unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource)) - if mayLoadTreesFromTasty then - classRoot.classSymbol.rootTreeOrProvider = unpickler - moduleRoot.classSymbol.rootTreeOrProvider = unpickler - checkTastyUUID(tastyFile, tastyBytes) + try + val (classRoot, moduleRoot) = rootDenots(root.asClass) + val tastyBytes = tastyFile.toByteArray + val unpickler = new tasty.DottyUnpickler(tastyBytes) + unpickler.enter(roots = Set(classRoot, moduleRoot, moduleRoot.sourceModule))(using ctx.withSource(util.NoSource)) + if mayLoadTreesFromTasty then + classRoot.classSymbol.rootTreeOrProvider = unpickler + moduleRoot.classSymbol.rootTreeOrProvider = unpickler + checkTastyUUID(tastyFile, tastyBytes) + catch case e: RuntimeException => + val message = e match + case e: UnpickleException => + i"""TASTy file ${tastyFile.canonicalPath} could not be read, failing with: + | ${Option(e.getMessage).getOrElse("")}""" + case _ => + i"""TASTy file ${tastyFile.canonicalPath} is broken, reading aborted with ${e.getClass} + | ${Option(e.getMessage).getOrElse("")}""" + if (ctx.debug) e.printStackTrace() + throw IOException(message) private def checkTastyUUID(tastyFile: AbstractFile, tastyBytes: Array[Byte])(using Context): Unit = @@ -436,7 +448,7 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader { val className = tastyFile.name.stripSuffix(".tasty") tastyFile.resolveSibling(className + ".class") if classfile != null then - val tastyUUID = new TastyHeaderUnpickler(tastyBytes).readHeader() + val tastyUUID = new TastyHeaderUnpickler(TastyUnpickler.scala3CompilerConfig, tastyBytes).readHeader() new ClassfileTastyUUIDParser(classfile)(ctx).checkTastyUUID(tastyUUID) else // This will be the case in any of our tests that compile with `-Youtput-only-tasty` diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala index 70bdec7780e2..679df42daca8 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala @@ -4,7 +4,8 @@ package tasty import scala.language.unsafeNulls -import dotty.tools.tasty.{TastyFormat, TastyBuffer, TastyReader, TastyHeaderUnpickler} +import dotty.tools.tasty.{TastyFormat, TastyBuffer, TastyReader, TastyHeaderUnpickler, UnpicklerConfig} +import TastyHeaderUnpickler.TastyVersion import TastyFormat.NameTags._, TastyFormat.nameTagToString import TastyBuffer.NameRef @@ -24,6 +25,39 @@ object TastyUnpickler { def apply(ref: NameRef): TermName = names(ref.index) def contents: Iterable[TermName] = names } + + trait Scala3CompilerConfig extends UnpicklerConfig: + private def asScala3Compiler(version: TastyVersion): String = + if (version.major == 28) { + // scala 3.x.y series + if (version.experimental > 0) + // scenario here is someone using 3.4.0 to read 3.4.1-RC1-NIGHTLY, in this case, we should show 3.4 nightly. + s"the same nightly or snapshot Scala 3.${version.minor - 1} compiler" + else s"a Scala 3.${version.minor}.0 compiler or newer" + } + else if (version.experimental > 0) "the same Scala compiler" // unknown major version, just say same + else "a more recent Scala compiler" // unknown major version, just say later + + /** The description of the upgraded scala compiler that can read the given TASTy version */ + final def upgradedReaderTool(version: TastyVersion): String = asScala3Compiler(version) + + /** The description of the upgraded scala compiler that can produce the given TASTy version */ + final def upgradedProducerTool(version: TastyVersion): String = asScala3Compiler(version) + + final def recompileAdditionalInfo: String = """ + | Usually this means that the library dependency containing this file should be updated.""".stripMargin + + final def upgradeAdditionalInfo(fileVersion: TastyVersion): String = + if (fileVersion.isExperimental && experimentalVersion == 0) { + """ + | Note that you are using a stable compiler, which can not read experimental TASTy.""".stripMargin + } + else "" + end Scala3CompilerConfig + + /** A config for the TASTy reader of a scala 3 compiler */ + val scala3CompilerConfig: UnpicklerConfig = new Scala3CompilerConfig with UnpicklerConfig.DefaultTastyVersion {} + } import TastyUnpickler._ @@ -88,7 +122,7 @@ class TastyUnpickler(reader: TastyReader) { result } - new TastyHeaderUnpickler(reader).readHeader() + new TastyHeaderUnpickler(scala3CompilerConfig, reader).readHeader() locally { until(readEnd()) { nameAtRef.add(readNameContents()) } diff --git a/compiler/test/dotty/tools/dotc/core/tasty/TastyHeaderUnpicklerTest.scala b/compiler/test/dotty/tools/dotc/core/tasty/TastyHeaderUnpicklerTest.scala new file mode 100644 index 000000000000..53c1f40638a4 --- /dev/null +++ b/compiler/test/dotty/tools/dotc/core/tasty/TastyHeaderUnpicklerTest.scala @@ -0,0 +1,311 @@ +package dotty.tools.dotc.core.tasty + +import org.junit.Assert._ +import org.junit.{Test, Ignore} + +import dotty.tools.tasty.TastyFormat._ +import dotty.tools.tasty.TastyBuffer._ +import dotty.tools.tasty.TastyBuffer +import dotty.tools.tasty.TastyReader +import dotty.tools.tasty.UnpickleException +import dotty.tools.tasty.TastyHeaderUnpickler +import dotty.tools.tasty.TastyHeaderUnpickler.TastyVersion +import dotty.tools.tasty.UnpicklerConfig + +class TastyHeaderUnpicklerTest { + + import TastyHeaderUnpicklerTest._ + + @Test + def okThisCompilerReadsItself: Unit = { + val file = TastyVersion(MajorVersion, MinorVersion, ExperimentalVersion) + val read = TastyVersion(MajorVersion, MinorVersion, ExperimentalVersion) + runTest(file, read, "Scala (current)") + } + + @Test + def okExperimentalCompilerReadsItself: Unit = { + val file = TastyVersion(MajorVersion, MinorVersion, 1) + val read = TastyVersion(MajorVersion, MinorVersion, 1) + runTest(file, read, "Scala (current)") + } + + @Test + def okStableCompilerReadsItself: Unit = { + val file = TastyVersion(MajorVersion, MinorVersion, 0) + val read = TastyVersion(MajorVersion, MinorVersion, 0) + runTest(file, read, "Scala (current)") + } + + @Test + def okReadOldStableMinorFromStable: Unit = { + val file = TastyVersion(28, 2, 0) + val read = TastyVersion(28, 3, 0) + runTest(file, read, "Scala 3.2.2") + } + + @Test + def okReadOldStableMinorFromExperimental: Unit = { + val file = TastyVersion(28, 2, 0) + val read = TastyVersion(28, 3, 1) + runTest(file, read, "Scala 3.2.2") + } + + @Test + def failReadExperimentalFromStableSameMinor: Unit = { + val file = TastyVersion(28, 4, 1) + val read = TastyVersion(28, 4, 0) + expectUnpickleError(runTest(file, read, "Scala 3.4.0-RC1-bin-SNAPSHOT")) { + """Backward incompatible TASTy file has version 28.4-experimental-1, produced by Scala 3.4.0-RC1-bin-SNAPSHOT, + | expected stable TASTy from 28.0 to 28.4. + | The source of this file should be recompiled by a Scala 3.4.0 compiler or newer. + | Usually this means that the library dependency containing this file should be updated. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadExperimentalFromOldMinor: Unit = { + val file = TastyVersion(28, 3, 1) + val read = TastyVersion(28, 4, 0) + expectUnpickleError(runTest(file, read, "Scala 3.2.1-RC1-bin-SNAPSHOT")) { + """Backward incompatible TASTy file has version 28.3-experimental-1, produced by Scala 3.2.1-RC1-bin-SNAPSHOT, + | expected stable TASTy from 28.0 to 28.4. + | The source of this file should be recompiled by a Scala 3.3.0 compiler or newer. + | Usually this means that the library dependency containing this file should be updated. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadOldMajor: Unit = { + val file = TastyVersion(27, 3, 0) + val read = TastyVersion(28, 3, 0) + expectUnpickleError(runTest(file, read, "Scala 3.0.0-M1")) { + """Backward incompatible TASTy file has version 27.3, + | expected stable TASTy from 28.0 to 28.3. + | The source of this file should be recompiled by a Scala 3.0.0 compiler or newer. + | Usually this means that the library dependency containing this file should be updated. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadOldMajor_generic: Unit = { + // We check the generic version here because it will produce a different message. + val file = TastyVersion(27, 3, 0) + val read = TastyVersion(28, 3, 0) + expectUnpickleError(runTest(file, read, "Scala 3.0.0-M1", generic = true)) { + """Backward incompatible TASTy file has version 27.3, + | expected stable TASTy from 28.0 to 28.3. + | The source of this file should be recompiled by a later version. + | Usually this means that the classpath entry of this file should be updated. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadOldExperimentalFromSameMinorWhileExperimental: Unit = { + val file = TastyVersion(28, 4, 1) + val read = TastyVersion(28, 4, 2) + expectUnpickleError(runTest(file, read, "Scala 3.3.3-RC1-NIGHTLY")) { + """Backward incompatible TASTy file has version 28.4-experimental-1, produced by Scala 3.3.3-RC1-NIGHTLY, + | expected stable TASTy from 28.0 to 28.3, or exactly 28.4-experimental-2. + | The source of this file should be recompiled by the same nightly or snapshot Scala 3.3 compiler. + | Usually this means that the library dependency containing this file should be updated. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadOldExperimentalFromSameMinorWhileExperimental_generic: Unit = { + // We check the generic version here because it will produce a different message. + val file = TastyVersion(28, 4, 1) + val read = TastyVersion(28, 4, 2) + expectUnpickleError(runTest(file, read, "Scala 3.3.3-RC1-NIGHTLY", generic = true)) { + """Backward incompatible TASTy file has version 28.4-experimental-1, produced by Scala 3.3.3-RC1-NIGHTLY, + | expected stable TASTy from 28.0 to 28.3, or exactly 28.4-experimental-2. + | The source of this file should be recompiled by a later version. + | Usually this means that the classpath entry of this file should be updated. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadNewerStableMinorFromStable: Unit = { + val file = TastyVersion(28, 3, 0) + val read = TastyVersion(28, 2, 0) + expectUnpickleError(runTest(file, read, "Scala 3.3.1")) { + """Forward incompatible TASTy file has version 28.3, produced by Scala 3.3.1, + | expected stable TASTy from 28.0 to 28.2. + | To read this TASTy file, use a Scala 3.3.0 compiler or newer. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadNewerStableMinorFromStable_generic: Unit = { + // We check the generic version here because it will produce a different message. + val file = TastyVersion(28, 3, 0) + val read = TastyVersion(28, 2, 0) + expectUnpickleError(runTest(file, read, "Scala 3.3.1", generic = true)) { + """Forward incompatible TASTy file has version 28.3, produced by Scala 3.3.1, + | expected stable TASTy from 28.0 to 28.2. + | To read this TASTy file, use a newer version of this tool compatible with TASTy 28.3. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadNewerExperimentalMinorFromStable: Unit = { + val file = TastyVersion(28, 3, 1) + val read = TastyVersion(28, 2, 0) + expectUnpickleError(runTest(file, read, "Scala 3.2.2-RC1-NIGHTLY")) { + """Forward incompatible TASTy file has version 28.3-experimental-1, produced by Scala 3.2.2-RC1-NIGHTLY, + | expected stable TASTy from 28.0 to 28.2. + | To read this experimental TASTy file, use the same nightly or snapshot Scala 3.2 compiler. + | Note that you are using a stable compiler, which can not read experimental TASTy. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadNewerStableMajor: Unit = { + val file = TastyVersion(29, 0, 0) + val read = TastyVersion(28, 3, 0) + expectUnpickleError(runTest(file, read, "Scala 4.0.0")) { + """Forward incompatible TASTy file has version 29.0, produced by Scala 4.0.0, + | expected stable TASTy from 28.0 to 28.3. + | To read this TASTy file, use a more recent Scala compiler. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadNewerExperimentalMajor: Unit = { + val file = TastyVersion(29, 0, 1) + val read = TastyVersion(28, 3, 0) + expectUnpickleError(runTest(file, read, "Scala 4.0.0-M1")) { + """Forward incompatible TASTy file has version 29.0-experimental-1, produced by Scala 4.0.0-M1, + | expected stable TASTy from 28.0 to 28.3. + | To read this experimental TASTy file, use the same Scala compiler. + | Note that you are using a stable compiler, which can not read experimental TASTy. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadNewerExperimentalMajor_generic: Unit = { + // We check the generic version here because it will produce a different message. + val file = TastyVersion(29, 0, 1) + val read = TastyVersion(28, 3, 0) + expectUnpickleError(runTest(file, read, "Scala 4.0.0-M1", generic = true)) { + """Forward incompatible TASTy file has version 29.0-experimental-1, produced by Scala 4.0.0-M1, + | expected stable TASTy from 28.0 to 28.3. + | To read this experimental TASTy file, use the version of this tool compatible with TASTy 29.0-experimental-1. + | Note that this tool does not support reading experimental TASTy. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadStableFromExperimentalSameMinor: Unit = { + val file = TastyVersion(28, 4, 0) + val read = TastyVersion(28, 4, 1) // 3.4.0-RC1-NIGHTLY + expectUnpickleError(runTest(file, read, "Scala 3.4.2")) { + """Forward incompatible TASTy file has version 28.4, produced by Scala 3.4.2, + | expected stable TASTy from 28.0 to 28.3, or exactly 28.4-experimental-1. + | To read this TASTy file, use a Scala 3.4.0 compiler or newer. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadNewerExperimentalFromExperimentalSameMinor: Unit = { + val file = TastyVersion(28, 4, 2) + val read = TastyVersion(28, 4, 1) + expectUnpickleError(runTest(file, read, "Scala 3.3.3-RC2-NIGHTLY")) { + """Forward incompatible TASTy file has version 28.4-experimental-2, produced by Scala 3.3.3-RC2-NIGHTLY, + | expected stable TASTy from 28.0 to 28.3, or exactly 28.4-experimental-1. + | To read this experimental TASTy file, use the same nightly or snapshot Scala 3.3 compiler. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + + @Test + def failReadNewerExperimentalFromExperimentalSameMinor_generic: Unit = { + // We check the generic version here because it will produce a different message. + val file = TastyVersion(28, 4, 2) + val read = TastyVersion(28, 4, 1) + expectUnpickleError(runTest(file, read, "Scala 3.3.3-RC2-NIGHTLY", generic = true)) { + """Forward incompatible TASTy file has version 28.4-experimental-2, produced by Scala 3.3.3-RC2-NIGHTLY, + | expected stable TASTy from 28.0 to 28.3, or exactly 28.4-experimental-1. + | To read this experimental TASTy file, use the version of this tool compatible with TASTy 28.4-experimental-2. + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin + } + } + +} + +object TastyHeaderUnpicklerTest { + + def fillHeader(maj: Int, min: Int, exp: Int, compiler: String): TastyBuffer = { + val compilerBytes = compiler.getBytes(java.nio.charset.StandardCharsets.UTF_8).nn + val buf = new TastyBuffer(header.length + 32 + compilerBytes.length) + for (ch <- header) buf.writeByte(ch.toByte) + buf.writeNat(maj) + buf.writeNat(min) + buf.writeNat(exp) + buf.writeNat(compilerBytes.length) + buf.writeBytes(compilerBytes, compilerBytes.length) + buf.writeUncompressedLong(237478L) + buf.writeUncompressedLong(324789L) + buf + } + + case class CustomScalaConfig(compilerVersion: TastyVersion) extends TastyUnpickler.Scala3CompilerConfig { + override def majorVersion: Int = compilerVersion.major + override def minorVersion: Int = compilerVersion.minor + override def experimentalVersion: Int = compilerVersion.experimental + } + + case class CustomGenericConfig(compilerVersion: TastyVersion) extends UnpicklerConfig.Generic { + override def majorVersion: Int = compilerVersion.major + override def minorVersion: Int = compilerVersion.minor + override def experimentalVersion: Int = compilerVersion.experimental + } + + def runTest(file: TastyVersion, read: TastyVersion, compiler: String, generic: Boolean = false): Unit = { + val headerBuffer = fillHeader(file.major, file.minor, file.experimental, compiler) + val bs = headerBuffer.bytes.clone + val config = if (generic) CustomGenericConfig(read) else CustomScalaConfig(read) + val hr = new TastyHeaderUnpickler(config, new TastyReader(bs)) + hr.readFullHeader() + } + + def expectUnpickleError(op: => Unit)(message: String) = { + try { + op + fail() + } + catch { + case err: UnpickleException => assert(err.getMessage.nn.contains(message)) + } + } + +} diff --git a/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala b/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala index c0ed5dbd58fa..db07666d3be1 100644 --- a/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala +++ b/tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala @@ -3,6 +3,7 @@ package dotty.tools.tasty import java.util.UUID import TastyFormat.{MajorVersion, MinorVersion, ExperimentalVersion, header} +import TastyHeaderUnpickler.TastyVersion /** * The Tasty Header consists of four fields: @@ -27,12 +28,67 @@ sealed abstract case class TastyHeader( toolingVersion: String ) -class TastyHeaderUnpickler(reader: TastyReader) { +trait UnpicklerConfig { + /** The TASTy major version that this reader supports */ + def majorVersion: Int + /** The TASTy minor version that this reader supports */ + def minorVersion: Int + /** The TASTy experimental version that this reader supports */ + def experimentalVersion: Int + /** The description of the upgraded tool that can read the given TASTy version */ + def upgradedReaderTool(version: TastyVersion): String + /** The description of the upgraded tool that can produce the given TASTy version */ + def upgradedProducerTool(version: TastyVersion): String + /** Additional information to help a user fix the outdated TASTy problem */ + def recompileAdditionalInfo: String + /** Additional information to help a user fix the more recent TASTy problem */ + def upgradeAdditionalInfo(fileVersion: TastyVersion): String +} + +object UnpicklerConfig { + + /** A config where its major, minor and experimental versions are fixed to those in TastyFormat */ + trait DefaultTastyVersion extends UnpicklerConfig { + override final def majorVersion: Int = MajorVersion + override final def minorVersion: Int = MinorVersion + override final def experimentalVersion: Int = ExperimentalVersion + } + + trait Generic extends UnpicklerConfig { + final def upgradedProducerTool(version: TastyVersion): String = + "a later version" + + final def upgradedReaderTool(version: TastyVersion): String = + if (version.isExperimental) s"the version of this tool compatible with TASTy ${version.show}" + else s"a newer version of this tool compatible with TASTy ${version.show}" + + final def recompileAdditionalInfo: String = """ + | Usually this means that the classpath entry of this file should be updated.""".stripMargin + + final def upgradeAdditionalInfo(fileVersion: TastyVersion): String = + if (fileVersion.isExperimental && experimentalVersion == 0) { + """ + | Note that this tool does not support reading experimental TASTy.""".stripMargin + } + else "" + } + + /** A config for the TASTy reader of a generic tool */ + val generic: UnpicklerConfig = new UnpicklerConfig with Generic with DefaultTastyVersion {} +} + +class TastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReader) { import TastyHeaderUnpickler._ import reader._ + def this(config: UnpicklerConfig, bytes: Array[Byte]) = this(config, new TastyReader(bytes)) + def this(reader: TastyReader) = this(UnpicklerConfig.generic, reader) def this(bytes: Array[Byte]) = this(new TastyReader(bytes)) + private val toolMajor: Int = config.majorVersion + private val toolMinor: Int = config.minorVersion + private val toolExperimental: Int = config.experimentalVersion + /** reads and verifies the TASTy version, extracting the UUID */ def readHeader(): UUID = readFullHeader().uuid @@ -45,8 +101,11 @@ class TastyHeaderUnpickler(reader: TastyReader) { val fileMajor = readNat() if (fileMajor <= 27) { // old behavior before `tasty-core` 3.0.0-M4 val fileMinor = readNat() - val signature = signatureString(fileMajor, fileMinor, 0) - throw new UnpickleException(signature + backIncompatAddendum + toolingAddendum) + val fileVersion = TastyVersion(fileMajor, fileMinor, 0) + val toolVersion = TastyVersion(toolMajor, toolMinor, toolExperimental) + val signature = signatureString(fileVersion, toolVersion, what = "Backward", tool = None) + val fix = recompileFix(toolVersion.minStable) + throw new UnpickleException(signature + fix + tastyAddendum) } else { val fileMinor = readNat() @@ -63,20 +122,38 @@ class TastyHeaderUnpickler(reader: TastyReader) { fileMajor = fileMajor, fileMinor = fileMinor, fileExperimental = fileExperimental, - compilerMajor = MajorVersion, - compilerMinor = MinorVersion, - compilerExperimental = ExperimentalVersion + compilerMajor = toolMajor, + compilerMinor = toolMinor, + compilerExperimental = toolExperimental ) check(validVersion, { - val signature = signatureString(fileMajor, fileMinor, fileExperimental) - val producedByAddendum = s"\nThe TASTy file was produced by $toolingVersion.$toolingAddendum" - val msg = ( - if (fileExperimental != 0) unstableAddendum - else if (fileMajor < MajorVersion) backIncompatAddendum - else forwardIncompatAddendum + // failure means that the TASTy file cannot be read, therefore it is either: + // - backwards incompatible major, in which case the library should be recompiled by the minimum stable minor + // version supported by this compiler + // - any experimental in an older minor, in which case the library should be recompiled by the stable + // compiler in the same minor. + // - older experimental in the same minor, in which case the compiler is also experimental, and the library + // should be recompiled by the current compiler + // - forward incompatible, in which case the compiler must be upgraded to the same version as the file. + val fileVersion = TastyVersion(fileMajor, fileMinor, fileExperimental) + val toolVersion = TastyVersion(toolMajor, toolMinor, toolExperimental) + + val compat = Compatibility.failReason(file = fileVersion, read = toolVersion) + + val what = if (compat < 0) "Backward" else "Forward" + val signature = signatureString(fileVersion, toolVersion, what, tool = Some(toolingVersion)) + val fix = ( + if (compat < 0) { + val newCompiler = + if (compat == Compatibility.BackwardIncompatibleMajor) toolVersion.minStable + else if (compat == Compatibility.BackwardIncompatibleExperimental) fileVersion.nextStable + else toolVersion // recompile the experimental library with the current experimental compiler + recompileFix(newCompiler) + } + else upgradeFix(fileVersion) ) - signature + msg + producedByAddendum + signature + fix + tastyAddendum }) val uuid = new UUID(readUncompressedLong(), readUncompressedLong()) @@ -89,40 +166,75 @@ class TastyHeaderUnpickler(reader: TastyReader) { private def check(cond: Boolean, msg: => String): Unit = { if (!cond) throw new UnpickleException(msg) } + + private def signatureString( + fileVersion: TastyVersion, toolVersion: TastyVersion, what: String, tool: Option[String]) = { + val optProducedBy = tool.fold("")(t => s", produced by $t") + s"""$what incompatible TASTy file has version ${fileVersion.show}$optProducedBy, + | expected ${toolVersion.validRange}. + |""".stripMargin + } + + private def recompileFix(producerVersion: TastyVersion) = { + val addendum = config.recompileAdditionalInfo + val newTool = config.upgradedProducerTool(producerVersion) + s""" The source of this file should be recompiled by $newTool.$addendum""".stripMargin + } + + private def upgradeFix(fileVersion: TastyVersion) = { + val addendum = config.upgradeAdditionalInfo(fileVersion) + val newTool = config.upgradedReaderTool(fileVersion) + s""" To read this ${fileVersion.kind} file, use $newTool.$addendum""".stripMargin + } + + private def tastyAddendum: String = """ + | Please refer to the documentation for information on TASTy versioning: + | https://docs.scala-lang.org/scala3/reference/language-versions/binary-compatibility.html""".stripMargin } object TastyHeaderUnpickler { - private def toolingAddendum = ( - if (ExperimentalVersion > 0) - "\nNote that your tooling is currently using an unstable TASTy version." - else - "" - ) - - private def signatureString(fileMajor: Int, fileMinor: Int, fileExperimental: Int) = { - def showMinorVersion(min: Int, exp: Int) = { - val expStr = if (exp == 0) "" else s" [unstable release: $exp]" - s"$min$expStr" - } - val minorVersion = showMinorVersion(MinorVersion, ExperimentalVersion) - val fileMinorVersion = showMinorVersion(fileMinor, fileExperimental) - s"""TASTy signature has wrong version. - | expected: {majorVersion: $MajorVersion, minorVersion: $minorVersion} - | found : {majorVersion: $fileMajor, minorVersion: $fileMinorVersion} - | - |""".stripMargin + private object Compatibility { + final val BackwardIncompatibleMajor = -3 + final val BackwardIncompatibleExperimental = -2 + final val ExperimentalRecompile = -1 + final val ExperimentalUpgrade = 1 + final val ForwardIncompatible = 2 + + /** Given that file can't be read, extract the reason */ + def failReason(file: TastyVersion, read: TastyVersion): Int = + if (file.major == read.major && file.minor == read.minor && file.isExperimental && read.isExperimental) { + if (file.experimental < read.experimental) ExperimentalRecompile // recompile library as compiler is too new + else ExperimentalUpgrade // they should upgrade compiler as library is too new + } + else if (file.major < read.major) + BackwardIncompatibleMajor // pre 3.0.0 + else if (file.isExperimental && file.major == read.major && file.minor <= read.minor) + // e.g. 3.4.0 reading 3.4.0-RC1-NIGHTLY, or 3.3.0 reading 3.0.2-RC1-NIGHTLY + BackwardIncompatibleExperimental + else ForwardIncompatible } - private def unstableAddendum = - """This TASTy file was produced by an unstable release. - |To read this TASTy file, your tooling must be at the same version.""".stripMargin + case class TastyVersion(major: Int, minor: Int, experimental: Int) { + def isExperimental: Boolean = experimental > 0 + + def nextStable: TastyVersion = copy(experimental = 0) - private def backIncompatAddendum = - """This TASTy file was produced by an earlier release that is not supported anymore. - |Please recompile this TASTy with a later version.""".stripMargin + def minStable: TastyVersion = copy(minor = 0, experimental = 0) + + def show: String = { + val suffix = if (isExperimental) s"-experimental-$experimental" else "" + s"$major.$minor$suffix" + } - private def forwardIncompatAddendum = - """This TASTy file was produced by a more recent, forwards incompatible release. - |To read this TASTy file, please upgrade your tooling.""".stripMargin + def kind: String = + if (isExperimental) "experimental TASTy" else "TASTy" + + def validRange: String = { + val min = TastyVersion(major, 0, 0) + val max = if (experimental == 0) this else TastyVersion(major, minor - 1, 0) + val extra = Option.when(experimental > 0)(this) + s"stable TASTy from ${min.show} to ${max.show}${extra.fold("")(e => s", or exactly ${e.show}")}" + } + } } diff --git a/tasty/test/dotty/tools/tasty/TastyHeaderUnpicklerTest.scala b/tasty/test/dotty/tools/tasty/TastyHeaderUnpicklerTest.scala deleted file mode 100644 index 9f54c4b3061b..000000000000 --- a/tasty/test/dotty/tools/tasty/TastyHeaderUnpicklerTest.scala +++ /dev/null @@ -1,84 +0,0 @@ -package dotty.tools.tasty - -import org.junit.Assert._ -import org.junit.{Test, Ignore} - -import TastyFormat._ -import TastyBuffer._ - -@Ignore // comment if you want to experiment with error messages -class TastyHeaderUnpicklerTest { - - import TastyHeaderUnpicklerTest._ - - @Test def vanilla: Unit = { - runTest(MajorVersion, MinorVersion, ExperimentalVersion, "Scala 3.0.0-M4-bin-SNAPSHOT-git-12345") - } - - @Test def failBumpExperimental: Unit = { - (runTest(MajorVersion, MinorVersion, ExperimentalVersion + 1, "Scala 3.0.0-M4-bin-SNAPSHOT-git-12345")) - } - - @Test def failBumpMinor: Unit = { - (runTest(MajorVersion, MinorVersion + 1, ExperimentalVersion, "Scala 3.1.0-RC1")) - } - - @Test def failBumpMajor: Unit = { - (runTest(MajorVersion + 1, MinorVersion, ExperimentalVersion, "Scala 4.0.0-M1")) - } - - @Test def failBumpMajorFinal: Unit = { - (runTest(MajorVersion + 1, MinorVersion, 0, "Scala 4.0.0")) - } - - @Test def okSubtractExperimental: Unit = { - (runTest(MajorVersion, MinorVersion, ExperimentalVersion - 1, "Scala 3.0.0")) - } - - @Test def okSubtractMinor: Unit = { - (runTest(MajorVersion, MinorVersion - 1, ExperimentalVersion, "Scala 3.0.0-M4-bin-SNAPSHOT-git-12345")) - } - - @Test def failSubtractMajor: Unit = { - (runTest(MajorVersion - 1, MinorVersion, ExperimentalVersion, "Scala 3.0.0-M4-bin-SNAPSHOT-git-12345")) - } - -} - -object TastyHeaderUnpicklerTest { - - - def fillHeader(maj: Int, min: Int, exp: Int, compiler: String): TastyBuffer = { - val compilerBytes = compiler.getBytes(java.nio.charset.StandardCharsets.UTF_8) - val buf = new TastyBuffer(header.length + 32 + compilerBytes.length) - for (ch <- header) buf.writeByte(ch.toByte) - buf.writeNat(maj) - buf.writeNat(min) - buf.writeNat(exp) - buf.writeNat(compilerBytes.length) - buf.writeBytes(compilerBytes, compilerBytes.length) - buf.writeUncompressedLong(237478L) - buf.writeUncompressedLong(324789L) - buf - } - - def runTest(maj: Int, min: Int, exp: Int, compiler: String): Unit = { - val headerBuffer = fillHeader(maj, min, exp, compiler) - val bs = headerBuffer.bytes.clone - - val hr = new TastyHeaderUnpickler(bs) - - hr.readFullHeader() - } - - def expectUnpickleError(op: => Unit) = { - try { - op - fail() - } - catch { - case err: UnpickleException => () - } - } - -} diff --git a/tests/pos-with-compiler-cc/backend/jvm/GenBCode.scala b/tests/pos-with-compiler-cc/backend/jvm/GenBCode.scala index 71d007370fe7..1af7e5dd705a 100644 --- a/tests/pos-with-compiler-cc/backend/jvm/GenBCode.scala +++ b/tests/pos-with-compiler-cc/backend/jvm/GenBCode.scala @@ -26,7 +26,8 @@ import Decorators.em import java.io.DataOutputStream import java.nio.channels.ClosedByInterruptException -import dotty.tools.tasty.{ TastyBuffer, TastyHeaderUnpickler } +import dotty.tools.tasty.{ TastyBuffer, TastyHeaderUnpickler, UnpicklerConfig } +import dotty.tools.tasty.core.TastyUnpickler import scala.tools.asm import scala.tools.asm.Handle @@ -285,7 +286,7 @@ class GenBCodePipeline(val int: DottyBackendInterface, val primitives: DottyPrim throw ex finally outstream.close() - val uuid = new TastyHeaderUnpickler(binary()).readHeader() + val uuid = new TastyHeaderUnpickler(TastyUnpickler.scala3CompilerConfig, binary()).readHeader() val lo = uuid.getMostSignificantBits val hi = uuid.getLeastSignificantBits