Skip to content

Commit 0273336

Browse files
authored
Merge pull request #11343 from dotty-staging/tasty-version-scheme
new TASTy header format - add experimental version and tooling version fields
2 parents 33fca15 + 496127d commit 0273336

File tree

9 files changed

+415
-86
lines changed

9 files changed

+415
-86
lines changed

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,24 @@ trait PropertiesTrait {
6464
*/
6565
def versionNumberString: String = scalaPropOrEmpty("version.number")
6666

67-
/** The version number of the jar this was loaded from plus "version " prefix,
68-
* or "version (unknown)" if it cannot be determined.
67+
/** The version number of the jar this was loaded from,
68+
* or `"(unknown)"` if it cannot be determined.
6969
*/
70-
val versionString: String = {
70+
val simpleVersionString: String = {
7171
val v = scalaPropOrElse("version.number", "(unknown)")
72-
"version " + scalaPropOrElse("version.number", "(unknown)") + {
72+
v + (
7373
if (v.contains("SNAPSHOT") || v.contains("NIGHTLY"))
7474
"-git-" + scalaPropOrElse("git.hash", "(unknown)")
75-
else ""
76-
}
75+
else
76+
""
77+
)
7778
}
7879

80+
/** The version number of the jar this was loaded from plus `"version "` prefix,
81+
* or `"version (unknown)"` if it cannot be determined.
82+
*/
83+
val versionString: String = "version " + simpleVersionString
84+
7985
/** Whether the current version of compiler is experimental
8086
*
8187
* 1. Snapshot and nightly releases are experimental.

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ import core.Symbols.{Symbol, ClassSymbol}
1212
import ast.tpd
1313
import Decorators._
1414

15+
object TastyPickler {
16+
17+
private val versionStringBytes = {
18+
val compilerString = s"Scala ${config.Properties.simpleVersionString}"
19+
compilerString.getBytes(java.nio.charset.StandardCharsets.UTF_8)
20+
}
21+
22+
}
23+
1524
class TastyPickler(val rootCls: ClassSymbol) {
1625

1726
private val sections = new mutable.ArrayBuffer[(NameRef, TastyBuffer)]
@@ -37,10 +46,13 @@ class TastyPickler(val rootCls: ClassSymbol) {
3746
val uuidHi: Long = otherSectionHashes.fold(0L)(_ ^ _)
3847

3948
val headerBuffer = {
40-
val buf = new TastyBuffer(header.length + 24)
49+
val buf = new TastyBuffer(header.length + TastyPickler.versionStringBytes.length + 32)
4150
for (ch <- header) buf.writeByte(ch.toByte)
4251
buf.writeNat(MajorVersion)
4352
buf.writeNat(MinorVersion)
53+
buf.writeNat(ExperimentalVersion)
54+
buf.writeNat(TastyPickler.versionStringBytes.length)
55+
buf.writeBytes(TastyPickler.versionStringBytes, TastyPickler.versionStringBytes.length)
4456
buf.writeUncompressedLong(uuidLow)
4557
buf.writeUncompressedLong(uuidHi)
4658
buf

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ class CompilationTests {
248248
// "-source", "future", // TODO: re-enable once we allow : @unchecked in pattern definitions. Right now, lots of narrowing pattern definitions fail.
249249
))(libGroup)
250250

251-
val tastyCoreSources = sources(Paths.get("tasty/src")) ++ sources(Paths.get("tasty/src-bootstrapped"))
251+
val tastyCoreSources = sources(Paths.get("tasty/src"))
252252
val tastyCore = compileList("tastyCore", tastyCoreSources, opt)(tastyCoreGroup)
253253

254254
val compilerSources = sources(Paths.get("compiler/src")) ++ sources(Paths.get("compiler/src-bootstrapped"))

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

Lines changed: 0 additions & 46 deletions
This file was deleted.

tasty/src-non-bootstrapped/dotty/tools/tasty/TastyHeaderUnpickler.scala

Lines changed: 0 additions & 29 deletions
This file was deleted.

tasty/src/dotty/tools/tasty/TastyFormat.scala

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ Micro-syntax:
1919
2020
Macro-format:
2121
22-
File = Header majorVersion_Nat minorVersion_Nat UUID
22+
File = Header majorVersion_Nat minorVersion_Nat experimentalVersion_Nat VersionString UUID
2323
nameTable_Length Name* Section*
2424
Header = 0x5CA1AB1F
2525
UUID = Byte*16 -- random UUID
26+
VersionString = Length UTF8-CodePoint* -- string that represents the compiler that produced the TASTy
2627
2728
Section = NameRef Length Bytes
2829
Length = Nat -- length of rest of entry in bytes
@@ -262,9 +263,100 @@ Standard Section: "Comments" Comment*
262263

263264
object TastyFormat {
264265

266+
/** The first four bytes of a TASTy file, followed by four values:
267+
* - `MajorVersion: Int` - see definition in `TastyFormat`
268+
* - `MinorVersion: Int` - see definition in `TastyFormat`
269+
* - `ExperimentalVersion: Int` - see definition in `TastyFormat`
270+
* - `ToolingVersion: String` - arbitrary length string representing the tool that produced the TASTy.
271+
*/
265272
final val header: Array[Int] = Array(0x5C, 0xA1, 0xAB, 0x1F)
266-
val MajorVersion: Int = 27
267-
val MinorVersion: Int = 0
273+
274+
/**Natural number. Each increment of the `MajorVersion` begins a
275+
* new series of backward compatible TASTy versions.
276+
*
277+
* A TASTy file in either the preceeding or succeeding series is
278+
* incompatible with the current value.
279+
*/
280+
final val MajorVersion: Int = 28
281+
282+
/**Natural number. Each increment of the `MinorVersion`, within
283+
* a series declared by the `MajorVersion`, breaks forward
284+
* compatibility, but remains backwards compatible, with all
285+
* preceeding `MinorVersion`.
286+
*/
287+
final val MinorVersion: Int = 0
288+
289+
/**Natural Number. The `ExperimentalVersion` allows for
290+
* experimentation with changes to TASTy without committing
291+
* to any guarantees of compatibility.
292+
*
293+
* A zero value indicates that the TASTy version is from a
294+
* stable, final release.
295+
*
296+
* A strictly positive value indicates that the TASTy
297+
* version is experimental. An experimental TASTy file
298+
* can only be read by a tool with the same version.
299+
* However, tooling with an experimental TASTy version
300+
* is able to read final TASTy documents if the file's
301+
* `MinorVersion` is strictly less than the current value.
302+
*/
303+
final val ExperimentalVersion: Int = 1
304+
305+
/**This method implements a binary relation (`<:<`) between two TASTy versions.
306+
* We label the lhs `file` and rhs `compiler`.
307+
* if `file <:< compiler` then the TASTy file is valid to be read.
308+
*
309+
* TASTy versions have a partial order,
310+
* for example `a <:< b` and `b <:< a` are both false if `a` and `b` have different major versions.
311+
*
312+
* We follow the given algorithm:
313+
* ```
314+
* if file.major != compiler.major then
315+
* return incompatible
316+
* if compiler.experimental == 0 then
317+
* if file.experimental != 0 then
318+
* return incompatible
319+
* if file.minor > compiler.minor then
320+
* return incompatible
321+
* else
322+
* return compatible
323+
* else invariant[compiler.experimental != 0]
324+
* if file.experimental == compiler.experimental then
325+
* if file.minor == compiler.minor then
326+
* return compatible (all fields equal)
327+
* else
328+
* return incompatible
329+
* else if file.experimental == 0,
330+
* if file.minor < compiler.minor then
331+
* return compatible (an experimental version can read a previous released version)
332+
* else
333+
* return incompatible (an experimental version cannot read its own minor version or any later version)
334+
* else invariant[file.experimental is non-0 and different than compiler.experimental]
335+
* return incompatible
336+
* ```
337+
*/
338+
def isVersionCompatible(
339+
fileMajor: Int,
340+
fileMinor: Int,
341+
fileExperimental: Int,
342+
compilerMajor: Int,
343+
compilerMinor: Int,
344+
compilerExperimental: Int
345+
): Boolean = (
346+
fileMajor == compilerMajor && (
347+
if (fileExperimental == compilerExperimental) {
348+
if (compilerExperimental == 0) {
349+
fileMinor <= compilerMinor
350+
}
351+
else {
352+
fileMinor == compilerMinor
353+
}
354+
}
355+
else {
356+
fileExperimental == 0 && fileMinor < compilerMinor
357+
}
358+
)
359+
)
268360

269361
final val ASTsSection = "ASTs"
270362
final val PositionsSection = "Positions"
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package dotty.tools.tasty
2+
3+
import java.util.UUID
4+
5+
import TastyFormat.{MajorVersion, MinorVersion, ExperimentalVersion, header}
6+
7+
/**
8+
* The Tasty Header consists of four fields:
9+
* - uuid
10+
* - contains a hash of the sections of the TASTy file
11+
* - majorVersion
12+
* - matching the TASTy format version that last broke backwards compatibility
13+
* - minorVersion
14+
* - matching the TASTy format version that last broke forward compatibility
15+
* - experimentalVersion
16+
* - 0 for final compiler version
17+
* - positive for between minor versions and forward compatibility
18+
* is broken since the previous stable version.
19+
* - toolingVersion
20+
* - arbitrary string representing the tooling that produced the TASTy
21+
*/
22+
sealed abstract case class TastyHeader(
23+
uuid: UUID,
24+
majorVersion: Int,
25+
minorVersion: Int,
26+
experimentalVersion: Int,
27+
toolingVersion: String
28+
)
29+
30+
class TastyHeaderUnpickler(reader: TastyReader) {
31+
import TastyHeaderUnpickler._
32+
import reader._
33+
34+
def this(bytes: Array[Byte]) = this(new TastyReader(bytes))
35+
36+
/** reads and verifies the TASTy version, extracting the UUID */
37+
def readHeader(): UUID =
38+
readFullHeader().uuid
39+
40+
/** reads and verifies the TASTy version, extracting the whole header */
41+
def readFullHeader(): TastyHeader = {
42+
43+
for (i <- 0 until header.length)
44+
check(readByte() == header(i), "not a TASTy file")
45+
val fileMajor = readNat()
46+
if (fileMajor <= 27) { // old behavior before `tasty-core` 3.0.0-M4
47+
val fileMinor = readNat()
48+
val signature = signatureString(fileMajor, fileMinor, 0)
49+
throw new UnpickleException(signature + backIncompatAddendum + toolingAddendum)
50+
}
51+
else {
52+
val fileMinor = readNat()
53+
val fileExperimental = readNat()
54+
val toolingVersion = {
55+
val length = readNat()
56+
val start = currentAddr
57+
val end = start + length
58+
goto(end)
59+
new String(bytes, start.index, length)
60+
}
61+
62+
val validVersion = TastyFormat.isVersionCompatible(
63+
fileMajor = fileMajor,
64+
fileMinor = fileMinor,
65+
fileExperimental = fileExperimental,
66+
compilerMajor = MajorVersion,
67+
compilerMinor = MinorVersion,
68+
compilerExperimental = ExperimentalVersion
69+
)
70+
71+
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
78+
)
79+
signature + msg + producedByAddendum
80+
})
81+
82+
val uuid = new UUID(readUncompressedLong(), readUncompressedLong())
83+
new TastyHeader(uuid, fileMajor, fileMinor, fileExperimental, toolingVersion) {}
84+
}
85+
}
86+
87+
def isAtEnd: Boolean = reader.isAtEnd
88+
89+
private def check(cond: Boolean, msg: => String): Unit = {
90+
if (!cond) throw new UnpickleException(msg)
91+
}
92+
}
93+
94+
object TastyHeaderUnpickler {
95+
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
115+
}
116+
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
120+
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
124+
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
128+
}

0 commit comments

Comments
 (0)