Skip to content

Commit a6c5b17

Browse files
committed
improve error messages for mismatched tasty version
1 parent d19d054 commit a6c5b17

File tree

6 files changed

+405
-73
lines changed

6 files changed

+405
-73
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import StdNames.nme
2424
import java.io.DataOutputStream
2525
import java.nio.channels.ClosedByInterruptException
2626

27-
import dotty.tools.tasty.{ TastyBuffer, TastyHeaderUnpickler }
27+
import dotty.tools.tasty.{ TastyBuffer, TastyHeaderUnpickler, UnpicklerConfig }
2828

2929
import scala.tools.asm
3030
import scala.tools.asm.tree._
@@ -94,7 +94,7 @@ class CodeGen(val int: DottyBackendInterface, val primitives: DottyPrimitives)(
9494
for (binary <- unit.pickled.get(claszSymbol.asClass)) {
9595
generatedTasty += GeneratedTasty(store, binary)
9696
val tasty =
97-
val uuid = new TastyHeaderUnpickler(binary()).readHeader()
97+
val uuid = new TastyHeaderUnpickler(UnpicklerConfig.scala3Compiler, binary()).readHeader()
9898
val lo = uuid.getMostSignificantBits
9999
val hi = uuid.getLeastSignificantBits
100100

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import ast.desugar
2424

2525
import parsing.JavaParsers.OutlineJavaParser
2626
import parsing.Parsers.OutlineParser
27-
import dotty.tools.tasty.{TastyHeaderUnpickler, UnpickleException}
27+
import dotty.tools.tasty.{TastyHeaderUnpickler, UnpickleException, UnpicklerConfig}
2828

2929

3030
object SymbolLoaders {
@@ -447,7 +447,7 @@ class TastyLoader(val tastyFile: AbstractFile) extends SymbolLoader {
447447
val className = tastyFile.name.stripSuffix(".tasty")
448448
tastyFile.resolveSibling(className + ".class")
449449
if classfile != null then
450-
val tastyUUID = new TastyHeaderUnpickler(tastyBytes).readHeader()
450+
val tastyUUID = new TastyHeaderUnpickler(UnpicklerConfig.scala3Compiler, tastyBytes).readHeader()
451451
new ClassfileTastyUUIDParser(classfile)(ctx).checkTastyUUID(tastyUUID)
452452
else
453453
// This will be the case in any of our tests that compile with `-Youtput-only-tasty`

compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package tasty
44

55
import scala.language.unsafeNulls
66

7-
import dotty.tools.tasty.{TastyFormat, TastyBuffer, TastyReader, TastyHeaderUnpickler}
7+
import dotty.tools.tasty.{TastyFormat, TastyBuffer, TastyReader, TastyHeaderUnpickler, UnpicklerConfig}
88
import TastyFormat.NameTags._, TastyFormat.nameTagToString
99
import TastyBuffer.NameRef
1010

@@ -88,7 +88,7 @@ class TastyUnpickler(reader: TastyReader) {
8888
result
8989
}
9090

91-
new TastyHeaderUnpickler(reader).readHeader()
91+
new TastyHeaderUnpickler(UnpicklerConfig.scala3Compiler, reader).readHeader()
9292

9393
locally {
9494
until(readEnd()) { nameAtRef.add(readNameContents()) }

tasty/src/dotty/tools/tasty/TastyHeaderUnpickler.scala

Lines changed: 181 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dotty.tools.tasty
33
import java.util.UUID
44

55
import TastyFormat.{MajorVersion, MinorVersion, ExperimentalVersion, header}
6+
import TastyHeaderUnpickler.TastyVersion
67

78
/**
89
* The Tasty Header consists of four fields:
@@ -27,12 +28,99 @@ sealed abstract case class TastyHeader(
2728
toolingVersion: String
2829
)
2930

30-
class TastyHeaderUnpickler(reader: TastyReader) {
31+
trait UnpicklerConfig {
32+
/** The TASTy major version that this reader supports */
33+
def majorVersion: Int
34+
/** The TASTy minor version that this reader supports */
35+
def minorVersion: Int
36+
/** The TASTy experimental version that this reader supports */
37+
def experimentalVersion: Int
38+
/** The description of the upgraded tool that can read the given TASTy version */
39+
def upgradedReaderTool(version: TastyVersion): String
40+
/** The description of the upgraded tool that can produce the given TASTy version */
41+
def upgradedProducerTool(version: TastyVersion): String
42+
/** Additional information to help a user fix the outdated TASTy problem */
43+
def recompileAdditionalInfo: String
44+
/** Additional information to help a user fix the more recent TASTy problem */
45+
def upgradeAdditionalInfo(fileVersion: TastyVersion): String
46+
}
47+
48+
object UnpicklerConfig {
49+
50+
/** A config where its major, minor and experimental versions are fixed to those in TastyFormat */
51+
trait DefaultTastyVersion extends UnpicklerConfig {
52+
override final def majorVersion: Int = MajorVersion
53+
override final def minorVersion: Int = MinorVersion
54+
override final def experimentalVersion: Int = ExperimentalVersion
55+
}
56+
57+
trait Scala3Compiler extends UnpicklerConfig {
58+
private def asScala3Compiler(version: TastyVersion): String =
59+
if (version.major == 28) {
60+
// scala 3.x.y series
61+
if (version.experimental > 0)
62+
// scenario here is someone using 3.4.0 to read 3.4.1-RC1-NIGHTLY, in this case, we should show 3.4 nightly.
63+
s"the same nightly or snapshot Scala 3.${version.minor - 1} compiler"
64+
else s"a Scala 3.${version.minor}.0 compiler or newer"
65+
}
66+
else if (version.experimental > 0) "the same Scala compiler" // unknown major version, just say same
67+
else "a more recent Scala compiler" // unknown major version, just say later
68+
69+
/** The description of the upgraded scala compiler that can read the given TASTy version */
70+
final def upgradedReaderTool(version: TastyVersion): String = asScala3Compiler(version)
71+
72+
/** The description of the upgraded scala compiler that can produce the given TASTy version */
73+
final def upgradedProducerTool(version: TastyVersion): String = asScala3Compiler(version)
74+
75+
final def recompileAdditionalInfo: String = """
76+
| Usually this means that the library dependency containing this file should be updated.""".stripMargin
77+
78+
final def upgradeAdditionalInfo(fileVersion: TastyVersion): String =
79+
if (fileVersion.isExperimental && experimentalVersion == 0) {
80+
"""
81+
| Note that you are using a stable compiler, which can not read experimental TASTy.""".stripMargin
82+
}
83+
else ""
84+
}
85+
86+
trait Generic extends UnpicklerConfig {
87+
final def upgradedProducerTool(version: TastyVersion): String =
88+
"a later version"
89+
90+
final def upgradedReaderTool(version: TastyVersion): String =
91+
if (version.isExperimental) s"the version of this tool compatible with TASTy ${version.show}"
92+
else s"a newer version of this tool compatible with TASTy ${version.show}"
93+
94+
final def recompileAdditionalInfo: String = """
95+
| Usually this means that the classpath entry of this file should be updated.""".stripMargin
96+
97+
final def upgradeAdditionalInfo(fileVersion: TastyVersion): String =
98+
if (fileVersion.isExperimental && experimentalVersion == 0) {
99+
"""
100+
| Note that this tool does not support reading experimental TASTy.""".stripMargin
101+
}
102+
else ""
103+
}
104+
105+
/** A config for the TASTy reader of a scala 3 compiler */
106+
val scala3Compiler = new UnpicklerConfig with Scala3Compiler with DefaultTastyVersion {}
107+
108+
/** A config for the TASTy reader of a generic tool */
109+
val generic = new UnpicklerConfig with Generic with DefaultTastyVersion {}
110+
}
111+
112+
class TastyHeaderUnpickler(config: UnpicklerConfig, reader: TastyReader) {
31113
import TastyHeaderUnpickler._
32114
import reader._
33115

116+
def this(config: UnpicklerConfig, bytes: Array[Byte]) = this(config, new TastyReader(bytes))
117+
def this(reader: TastyReader) = this(UnpicklerConfig.generic, reader)
34118
def this(bytes: Array[Byte]) = this(new TastyReader(bytes))
35119

120+
private val toolMajor: Int = config.majorVersion
121+
private val toolMinor: Int = config.minorVersion
122+
private val toolExperimental: Int = config.experimentalVersion
123+
36124
/** reads and verifies the TASTy version, extracting the UUID */
37125
def readHeader(): UUID =
38126
readFullHeader().uuid
@@ -45,8 +133,11 @@ class TastyHeaderUnpickler(reader: TastyReader) {
45133
val fileMajor = readNat()
46134
if (fileMajor <= 27) { // old behavior before `tasty-core` 3.0.0-M4
47135
val fileMinor = readNat()
48-
val signature = signatureString(fileMajor, fileMinor, 0)
49-
throw new UnpickleException(signature + backIncompatAddendum + toolingAddendum)
136+
val fileVersion = TastyVersion(fileMajor, fileMinor, 0)
137+
val toolVersion = TastyVersion(toolMajor, toolMinor, toolExperimental)
138+
val signature = signatureString(fileVersion, toolVersion, what = "backward", tool = None)
139+
val fix = recompileFix(toolVersion.minStable)
140+
throw new UnpickleException(signature + fix)
50141
}
51142
else {
52143
val fileMinor = readNat()
@@ -63,20 +154,38 @@ class TastyHeaderUnpickler(reader: TastyReader) {
63154
fileMajor = fileMajor,
64155
fileMinor = fileMinor,
65156
fileExperimental = fileExperimental,
66-
compilerMajor = MajorVersion,
67-
compilerMinor = MinorVersion,
68-
compilerExperimental = ExperimentalVersion
157+
compilerMajor = toolMajor,
158+
compilerMinor = toolMinor,
159+
compilerExperimental = toolExperimental
69160
)
70161

71162
check(validVersion, {
72-
val signature = signatureString(fileMajor, fileMinor, fileExperimental)
73-
val producedByAddendum = s"\nThe TASTy file was produced by $toolingVersion.$toolingAddendum"
74-
val msg = (
75-
if (fileExperimental != 0) unstableAddendum
76-
else if (fileMajor < MajorVersion) backIncompatAddendum
77-
else forwardIncompatAddendum
163+
// failure means that the TASTy file is can not be read, therefore it is either:
164+
// - backwards incompatible major, in which case the library should be recompiled by the minimum stable minor
165+
// version supported by this compiler
166+
// - any experimental in an older minor, in which case the library should be recompiled by the stable
167+
// compiler in the same minor.
168+
// - older experimental in the same minor, in which case the compiler is also experimental, and the library
169+
// should be recompiled by the current compiler
170+
// - forward incompatible, in which case the compiler must be upgraded to the same version as the file.
171+
val fileVersion = TastyVersion(fileMajor, fileMinor, fileExperimental)
172+
val toolVersion = TastyVersion(toolMajor, toolMinor, toolExperimental)
173+
174+
val compat = Compatibility.failReason(file = fileVersion, read = toolVersion)
175+
176+
val what = if (compat < 0) "backward" else "forward"
177+
val signature = signatureString(fileVersion, toolVersion, what, tool = Some(toolingVersion))
178+
val fix = (
179+
if (compat < 0) {
180+
val newCompiler =
181+
if (compat == Compatibility.BackwardIncompatibleMajor) toolVersion.minStable
182+
else if (compat == Compatibility.BackwardIncompatibleExperimental) fileVersion.nextStable
183+
else toolVersion // recompile the experimental library with the current experimental compiler
184+
recompileFix(newCompiler)
185+
}
186+
else upgradeFix(fileVersion)
78187
)
79-
signature + msg + producedByAddendum
188+
signature + fix
80189
})
81190

82191
val uuid = new UUID(readUncompressedLong(), readUncompressedLong())
@@ -89,40 +198,71 @@ class TastyHeaderUnpickler(reader: TastyReader) {
89198
private def check(cond: Boolean, msg: => String): Unit = {
90199
if (!cond) throw new UnpickleException(msg)
91200
}
201+
202+
private def signatureString(
203+
fileVersion: TastyVersion, toolVersion: TastyVersion, what: String, tool: Option[String]) = {
204+
val optProducedBy = tool.fold("")(t => s" produced by $t")
205+
s"""TASTy file$optProducedBy has a $what incompatible TASTy version ${fileVersion.show},
206+
| expected ${toolVersion.validRange}.
207+
|""".stripMargin
208+
}
209+
210+
private def recompileFix(producerVersion: TastyVersion) = {
211+
val addendum = config.recompileAdditionalInfo
212+
val newTool = config.upgradedProducerTool(producerVersion)
213+
s""" The source of this file should be recompiled by $newTool.$addendum""".stripMargin
214+
}
215+
216+
private def upgradeFix(fileVersion: TastyVersion) = {
217+
val addendum = config.upgradeAdditionalInfo(fileVersion)
218+
val newTool = config.upgradedReaderTool(fileVersion)
219+
s""" To read this ${fileVersion.kind} file, use $newTool.$addendum""".stripMargin
220+
}
92221
}
93222

94223
object TastyHeaderUnpickler {
95224

96-
private def toolingAddendum = (
97-
if (ExperimentalVersion > 0)
98-
"\nNote that your tooling is currently using an unstable TASTy version."
99-
else
100-
""
101-
)
102-
103-
private def signatureString(fileMajor: Int, fileMinor: Int, fileExperimental: Int) = {
104-
def showMinorVersion(min: Int, exp: Int) = {
105-
val expStr = if (exp == 0) "" else s" [unstable release: $exp]"
106-
s"$min$expStr"
107-
}
108-
val minorVersion = showMinorVersion(MinorVersion, ExperimentalVersion)
109-
val fileMinorVersion = showMinorVersion(fileMinor, fileExperimental)
110-
s"""TASTy signature has wrong version.
111-
| expected: {majorVersion: $MajorVersion, minorVersion: $minorVersion}
112-
| found : {majorVersion: $fileMajor, minorVersion: $fileMinorVersion}
113-
|
114-
|""".stripMargin
225+
private object Compatibility {
226+
final val BackwardIncompatibleMajor = -3
227+
final val BackwardIncompatibleExperimental = -2
228+
final val ExperimentalRecompile = -1
229+
final val ExperimentalUpgrade = 1
230+
final val ForwardIncompatible = 2
231+
232+
/** Given that file can't be read, extract the reason */
233+
def failReason(file: TastyVersion, read: TastyVersion): Int =
234+
if (file.major == read.major && file.minor == read.minor && file.isExperimental && read.isExperimental) {
235+
if (file.experimental < read.experimental) ExperimentalRecompile // recompile library as compiler is too new
236+
else ExperimentalUpgrade // they should upgrade compiler as library is too new
237+
}
238+
else if (file.major < read.major)
239+
BackwardIncompatibleMajor // pre 3.0.0
240+
else if (file.isExperimental && file.major == read.major && file.minor <= read.minor)
241+
// e.g. 3.4.0 reading 3.4.0-RC1-NIGHTLY, or 3.3.0 reading 3.0.2-RC1-NIGHTLY
242+
BackwardIncompatibleExperimental
243+
else ForwardIncompatible
115244
}
116245

117-
private def unstableAddendum =
118-
"""This TASTy file was produced by an unstable release.
119-
|To read this TASTy file, your tooling must be at the same version.""".stripMargin
246+
case class TastyVersion(major: Int, minor: Int, experimental: Int) {
247+
def isExperimental: Boolean = experimental > 0
248+
249+
def nextStable: TastyVersion = copy(experimental = 0)
120250

121-
private def backIncompatAddendum =
122-
"""This TASTy file was produced by an earlier release that is not supported anymore.
123-
|Please recompile this TASTy with a later version.""".stripMargin
251+
def minStable: TastyVersion = copy(minor = 0, experimental = 0)
124252

125-
private def forwardIncompatAddendum =
126-
"""This TASTy file was produced by a more recent, forwards incompatible release.
127-
|To read this TASTy file, please upgrade your tooling.""".stripMargin
253+
def show: String = {
254+
val suffix = if (isExperimental) s"-experimental-$experimental" else ""
255+
s"$major.$minor$suffix"
256+
}
257+
258+
def kind: String =
259+
if (isExperimental) "experimental TASTy" else "TASTy"
260+
261+
def validRange: String = {
262+
val min = TastyVersion(major, 0, 0)
263+
val max = if (experimental == 0) this else TastyVersion(major, minor - 1, 0)
264+
val extra = Option.when(experimental > 0)(this)
265+
s"stable TASTy from ${min.show} to ${max.show}${extra.fold("")(e => s", or exactly ${e.show}")}"
266+
}
267+
}
128268
}

0 commit comments

Comments
 (0)