Skip to content

Commit ad7c20c

Browse files
committed
TASTy header format changes
Here we adds fields for experimental and tooling versions. - ExperimentalVersion: Nat, when reading non-zero and we are non-zero, the file must be at same version. Otherwise if we are non-zero, the file must have zero experimental version, and must be of strictly lower minor version. - ToolingVersion: String, an arbitrary UTF-8 string that says the name+version of the tooling that produced the file. We could reinterpret to mean the "minimum" version required to read that file, but that would require more process to record that.
1 parent 8b1188b commit ad7c20c

File tree

8 files changed

+373
-80
lines changed

8 files changed

+373
-80
lines changed

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 compiler ${config.Properties.versionString}"
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
@@ -247,7 +247,7 @@ class CompilationTests {
247247
// "-source", "3.1", // TODO: re-enable once we allow : @unchecked in pattern definitions. Right now, lots of narrowing pattern definitions fail.
248248
))(libGroup)
249249

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

253253
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: 21 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
@@ -263,8 +264,25 @@ Standard Section: "Comments" Comment*
263264
object TastyFormat {
264265

265266
final val header: Array[Int] = Array(0x5C, 0xA1, 0xAB, 0x1F)
266-
val MajorVersion: Int = 27
267-
val MinorVersion: Int = 0
267+
268+
/** Each increment of the MajorVersion begins a new series of backward compatible TASTy versions
269+
* - The MajorVersion is and will always be the first value in a TASTy document after the header bytes.
270+
* - A TASTy document can then be further parsed if and only if the MajorVersion read
271+
* from a TASTy file is equal to or less than this value.
272+
* - i.e. `MajorVersion` and `header` are the only stable parts of TASTy that can be used to decide how to parse
273+
* the rest of the document.
274+
*/
275+
final val MajorVersion: Int = 28
276+
277+
/** The last minor version to break forward compatability */
278+
final val MinorVersion: Int = 0
279+
280+
/** A transitionary marker:
281+
* - 0 means that `MinorVersion` is from a final release of TASTy
282+
* - A positive number means TASTy was produced by an experimental compiler, released in between
283+
* increasing `MinorVersion`.
284+
*/
285+
final val ExperimentalVersion: Int = 1
268286

269287
final val ASTsSection = "ASTs"
270288
final val PositionsSection = "Positions"
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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 // String could lead to a lot of duplication, perhaps cache headers?
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(expectedMinor, fileMajor, fileMinor.toString)
49+
throw new UnpickleException(signature + backIncompatAddendum + myAddendum)
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) // NOTE: new string object here (should probably cache as it leaks)
60+
}
61+
62+
val validVersion = ( // inlined from `dotty.tools.tasty.TastyVersionFormatTest.TastyVersion.<:<`
63+
fileMajor == MajorVersion && (
64+
if (fileExperimental == ExperimentalVersion) {
65+
if (ExperimentalVersion == 0) {
66+
fileMinor <= MinorVersion
67+
}
68+
else {
69+
fileMinor == MinorVersion
70+
}
71+
}
72+
else {
73+
fileExperimental == 0 && fileMinor < MinorVersion
74+
}
75+
)
76+
)
77+
check(validVersion, {
78+
val foundMinor = showMinorVersion(fileMinor, fileExperimental)
79+
val producedByAddendum = s"\nThe TASTy file was produced by $toolingVersion.$myAddendum"
80+
val signature = signatureString(expectedMinor, fileMajor, foundMinor)
81+
if (fileExperimental != 0)
82+
signature + unstableAddendum + producedByAddendum
83+
else if (fileMajor < MajorVersion)
84+
signature + backIncompatAddendum + producedByAddendum
85+
else
86+
signature + forwardIncompatAddendum + producedByAddendum
87+
}
88+
)
89+
val uuid = new UUID(readUncompressedLong(), readUncompressedLong())
90+
new TastyHeader(uuid, fileMajor, fileMinor, fileExperimental, toolingVersion) {}
91+
}
92+
}
93+
94+
def isAtEnd: Boolean = reader.isAtEnd
95+
96+
private def check(cond: Boolean, msg: => String): Unit = {
97+
if (!cond) throw new UnpickleException(msg)
98+
}
99+
}
100+
101+
object TastyHeaderUnpickler {
102+
103+
private def showMinorVersion(min: Int, exp: Int) = {
104+
val expStr = if (exp == 0) "" else s" [unstable release: $exp]"
105+
s"$min$expStr"
106+
}
107+
108+
private def expectedMinor = showMinorVersion(MinorVersion, ExperimentalVersion)
109+
110+
private def myAddendum = {
111+
if (ExperimentalVersion > 0)
112+
"\nNote that your tooling is currently using an unstable TASTy version."
113+
else
114+
""
115+
}
116+
117+
private def signatureString(minorVersion: String, fileMajor: Int, fileMinor: String) = (
118+
s"""TASTy signature has wrong version.
119+
| expected: {majorVersion: $MajorVersion, minorVersion: $minorVersion}
120+
| found : {majorVersion: $fileMajor, minorVersion: $fileMinor}
121+
|
122+
|""".stripMargin
123+
)
124+
125+
private def unstableAddendum =
126+
"""This TASTy file was produced by an unstable release.
127+
|To read this TASTy file, your tooling must be at the same version.""".stripMargin
128+
129+
private def backIncompatAddendum =
130+
"""This TASTy file was produced by a backwards incompatible release.
131+
|Please recompile this TASTy with a later version.""".stripMargin
132+
133+
private def forwardIncompatAddendum =
134+
"""This TASTy file was produced by a forwards incompatible release.
135+
|To read this TASTy file, please upgrade your tooling.""".stripMargin
136+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package dotty.tools.tasty
2+
3+
import org.junit.Assert._
4+
import org.junit.{Test, Ignore}
5+
6+
import TastyFormat._
7+
import TastyBuffer._
8+
9+
@Ignore // comment if you want to experiment with error messages
10+
class TastyHeaderUnpicklerTest {
11+
12+
import TastyHeaderUnpicklerTest._
13+
14+
@Test def vanilla: Unit = {
15+
runTest(MajorVersion, MinorVersion, ExperimentalVersion, "Scala compiler 3.0.0-M4-bin-SNAPSHOT-git-12345")
16+
}
17+
18+
@Test def failBumpExperimental: Unit = {
19+
(runTest(MajorVersion, MinorVersion, ExperimentalVersion + 1, "Scala compiler 3.0.0-M4-bin-SNAPSHOT-git-12345"))
20+
}
21+
22+
@Test def failBumpMinor: Unit = {
23+
(runTest(MajorVersion, MinorVersion + 1, ExperimentalVersion, "Scala compiler 3.1.0-RC1"))
24+
}
25+
26+
@Test def failBumpMajor: Unit = {
27+
(runTest(MajorVersion + 1, MinorVersion, ExperimentalVersion, "Scala compiler 4.0.0-M1"))
28+
}
29+
30+
@Test def failBumpMajorFinal: Unit = {
31+
(runTest(MajorVersion + 1, MinorVersion, 0, "Scala compiler 4.0.0"))
32+
}
33+
34+
@Test def okSubtractExperimental: Unit = {
35+
(runTest(MajorVersion, MinorVersion, ExperimentalVersion - 1, "Scala compiler 3.0.0"))
36+
}
37+
38+
@Test def okSubtractMinor: Unit = {
39+
(runTest(MajorVersion, MinorVersion - 1, ExperimentalVersion, "Scala compiler 3.0.0-M4-bin-SNAPSHOT-git-12345"))
40+
}
41+
42+
@Test def failSubtractMajor: Unit = {
43+
(runTest(MajorVersion - 1, MinorVersion, ExperimentalVersion, "Scala compiler 3.0.0-M4-bin-SNAPSHOT-git-12345"))
44+
}
45+
46+
}
47+
48+
object TastyHeaderUnpicklerTest {
49+
50+
51+
def fillHeader(maj: Int, min: Int, exp: Int, compiler: String): TastyBuffer = {
52+
val compilerBytes = compiler.getBytes(java.nio.charset.StandardCharsets.UTF_8)
53+
val buf = new TastyBuffer(header.length + 32 + compilerBytes.length)
54+
for (ch <- header) buf.writeByte(ch.toByte)
55+
buf.writeNat(maj)
56+
buf.writeNat(min)
57+
buf.writeNat(exp)
58+
buf.writeNat(compilerBytes.length)
59+
buf.writeBytes(compilerBytes, compilerBytes.length)
60+
buf.writeUncompressedLong(237478l)
61+
buf.writeUncompressedLong(324789l)
62+
buf
63+
}
64+
65+
def runTest(maj: Int, min: Int, exp: Int, compiler: String): Unit = {
66+
val headerBuffer = fillHeader(maj, min, exp, compiler)
67+
val bs = headerBuffer.bytes.clone
68+
69+
val hr = new TastyHeaderUnpickler(bs)
70+
71+
hr.readFullHeader()
72+
}
73+
74+
def expectUnpickleError(op: => Unit) = {
75+
try {
76+
op
77+
fail()
78+
}
79+
catch {
80+
case err: UnpickleException => ()
81+
}
82+
}
83+
84+
}

0 commit comments

Comments
 (0)