Skip to content

Commit 4eaddd5

Browse files
committed
TASTy header format - remove MinorVersion, add 3 new fields
1 parent 8b1188b commit 4eaddd5

File tree

6 files changed

+249
-80
lines changed

6 files changed

+249
-80
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@ class TastyPickler(val rootCls: ClassSymbol) {
3737
val uuidHi: Long = otherSectionHashes.fold(0L)(_ ^ _)
3838

3939
val headerBuffer = {
40-
val buf = new TastyBuffer(header.length + 24)
40+
val buf = new TastyBuffer(header.length + 32)
4141
for (ch <- header) buf.writeByte(ch.toByte)
4242
buf.writeNat(MajorVersion)
43-
buf.writeNat(MinorVersion)
43+
buf.writeNat(ScalaMajorVersion)
44+
buf.writeNat(ScalaMinorVersion)
45+
buf.writeNat(ExperimentalVersion)
4446
buf.writeUncompressedLong(uuidLow)
4547
buf.writeUncompressedLong(uuidHi)
4648
buf

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: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Micro-syntax:
1919
2020
Macro-format:
2121
22-
File = Header majorVersion_Nat minorVersion_Nat UUID
22+
File = Header majorVersion_Nat scalaMajorVersion_Nat scalaMinorVersion_Nat experimentalVersion_Nat UUID
2323
nameTable_Length Name* Section*
2424
Header = 0x5CA1AB1F
2525
UUID = Byte*16 -- random UUID
@@ -263,8 +263,28 @@ Standard Section: "Comments" Comment*
263263
object TastyFormat {
264264

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

269289
final val ASTsSection = "ASTs"
270290
final val PositionsSection = "Positions"
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package dotty.tools.tasty
2+
3+
import java.util.UUID
4+
5+
import TastyFormat.{MajorVersion, ScalaMajorVersion, ScalaMinorVersion, 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+
* - scalaMajorVersion
14+
* - matching Scala compiler that last broke forward compatibility
15+
* - scalaMinorVersion
16+
* - matching Scala compiler that last broke forward compatibility
17+
* - experimentalVersion
18+
* - 0 for final compiler version
19+
* - positive for between scala major/minor versions and forward compatibility
20+
* is broken since the previous stable version.
21+
*/
22+
sealed abstract case class TastyHeader(
23+
uuid: UUID,
24+
majorVersion: Int,
25+
scalaMajorVersion: Int,
26+
scalaMinorVersion: Int,
27+
experimentalVersion: Int
28+
)
29+
30+
class TastyHeaderUnpickler(reader: TastyReader) {
31+
import reader._
32+
33+
def this(bytes: Array[Byte]) = this(new TastyReader(bytes))
34+
35+
/** reads and verifies the TASTy version, extracting the UUID */
36+
def readHeader(): UUID =
37+
readFullHeader().uuid
38+
39+
/** reads and verifies the TASTy version, extracting the whole header */
40+
def readFullHeader(): TastyHeader = {
41+
42+
def showScalaVersion(maj: Int, min: Int, exp: Int) = {
43+
val expStr = if (exp == 0) "" else s" [unstable release: $exp]"
44+
s"$maj.$min$expStr"
45+
}
46+
47+
def expectedScala = showScalaVersion(ScalaMajorVersion, ScalaMinorVersion, ExperimentalVersion)
48+
49+
def myScalaV = s"$ScalaMajorVersion.$ScalaMinorVersion.x"
50+
51+
def myAddendum = {
52+
if (ExperimentalVersion > 0)
53+
"\nNote that you are currently using an unstable compiler version."
54+
else
55+
""
56+
}
57+
58+
for (i <- 0 until header.length)
59+
check(readByte() == header(i), "not a TASTy file")
60+
val majorVersion = readNat()
61+
if (majorVersion <= 27) { // old behavior before Scala 3.0.0-M4
62+
val minorVersion = readNat()
63+
throw new UnpickleException(
64+
s"""TASTy signature is from a backwards incompatible release.
65+
| expected: {majorVersion: $MajorVersion, scalaVersion: $expectedScala}
66+
| found : {majorVersion: $majorVersion, minorVersion: $minorVersion}
67+
|
68+
|Please recompile this TASTy with at least Scala $myScalaV.$myAddendum""".stripMargin
69+
)
70+
}
71+
else {
72+
check(majorVersion <= MajorVersion, // layout of header may have changed in future release
73+
s"""TASTy signature is from a future release of Scala with unknown TASTy format.
74+
| expected: {majorVersion: $MajorVersion, scalaVersion: $expectedScala}
75+
| found : {majorVersion: $majorVersion, ...}
76+
|
77+
|Please use TASTy compiled by Scala $myScalaV.$myAddendum""".stripMargin)
78+
val scalaMajorVersion = readNat()
79+
val scalaMinorVersion = readNat()
80+
val experimentalVersion = readNat()
81+
// |=============|==============|========|
82+
// | Our Version | Read Version | Valid |
83+
// |=============|==============|========|
84+
// | 3.0 @ 0 | 3.0 @ 0 | true |
85+
// | 3.0 @ 0 | 3.0 @ 1 | false |
86+
// | 3.0 @ 1 | 3.0 @ 0 | true |
87+
// | 3.0 | 3.0 | true |
88+
// | 3.0 | 3.1 | false |
89+
// | 3.1 | 3.0 | true |
90+
// | 4.0 | 3.0 | true |
91+
// | 4.0 | 3.1 | true |
92+
// | 4.1 | 3.0 | true |
93+
// | 3.0 | 4.0 | false |
94+
// | 3.0 | 4.1 | false |
95+
// | 3.1 | 4.0 | false |
96+
// |=============|==============|========|
97+
val validVersion = (
98+
majorVersion == MajorVersion
99+
&& scalaMajorVersion <= ScalaMajorVersion
100+
&& !(scalaMajorVersion == ScalaMajorVersion && scalaMinorVersion > ScalaMinorVersion)
101+
&& !(
102+
scalaMajorVersion == ScalaMajorVersion
103+
&& scalaMinorVersion == ScalaMinorVersion
104+
&& experimentalVersion > ExperimentalVersion
105+
)
106+
)
107+
check(validVersion, {
108+
val foundScala = showScalaVersion(scalaMajorVersion, scalaMinorVersion, experimentalVersion)
109+
val foundAddendum = {
110+
if (experimentalVersion > 0)
111+
"\nNote that this TASTy file was produced by an unstable compiler version."
112+
else
113+
""
114+
}
115+
if (majorVersion < MajorVersion) {
116+
s"""TASTy signature is from a backwards incompatible release.
117+
| expected: {majorVersion: $MajorVersion, scalaVersion: $expectedScala}
118+
| found : {majorVersion: $majorVersion, scalaVersion: $foundScala}
119+
|
120+
|Please recompile this TASTy with at least Scala $myScalaV.$myAddendum$foundAddendum""".stripMargin
121+
}
122+
else {
123+
val foundScalaV = s"$scalaMajorVersion.$scalaMinorVersion.x"
124+
s"""TASTy signature is from a forwards incompatible release.
125+
| expected: {majorVersion: $MajorVersion, scalaVersion: $expectedScala}
126+
| found : {majorVersion: $majorVersion, scalaVersion: $foundScala}
127+
|
128+
|To read this TASTy file, please upgrade your Scala version to at least $foundScalaV.$myAddendum$foundAddendum""".stripMargin
129+
}
130+
}
131+
)
132+
val uuid = new UUID(readUncompressedLong(), readUncompressedLong())
133+
new TastyHeader(uuid, majorVersion, scalaMajorVersion, scalaMinorVersion, experimentalVersion) {}
134+
}
135+
}
136+
137+
def isAtEnd: Boolean = reader.isAtEnd
138+
139+
private def check(cond: Boolean, msg: => String): Unit = {
140+
if (!cond) throw new UnpickleException(msg)
141+
}
142+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
def fillHeader(maj: Int, scalaMaj: Int, scalaMin: Int, exp: Int): TastyBuffer = {
13+
val buf = new TastyBuffer(header.length + 32)
14+
for (ch <- header) buf.writeByte(ch.toByte)
15+
buf.writeNat(maj)
16+
buf.writeNat(scalaMaj)
17+
buf.writeNat(scalaMin)
18+
buf.writeNat(exp)
19+
buf.writeUncompressedLong(237478l)
20+
buf.writeUncompressedLong(324789l)
21+
buf
22+
}
23+
24+
def runTest(maj: Int, scalaMaj: Int, scalaMin: Int, exp: Int): Unit = {
25+
val headerBuffer = fillHeader(maj, scalaMaj, scalaMin, exp)
26+
val bs = headerBuffer.bytes.clone
27+
28+
val hr = new TastyHeaderUnpickler(bs)
29+
30+
hr.readFullHeader()
31+
}
32+
33+
def expectUnpickleError(op: => Unit) = {
34+
try {
35+
op
36+
fail()
37+
}
38+
catch {
39+
case err: UnpickleException => ()
40+
}
41+
}
42+
43+
@Test def vanilla: Unit = {
44+
runTest(MajorVersion, ScalaMajorVersion, ScalaMinorVersion, ExperimentalVersion)
45+
}
46+
47+
@Test def failBumpExperimental: Unit = {
48+
(runTest(MajorVersion, ScalaMajorVersion, ScalaMinorVersion, ExperimentalVersion + 1))
49+
}
50+
51+
@Test def failBumpScalaMinor: Unit = {
52+
(runTest(MajorVersion, ScalaMajorVersion, ScalaMinorVersion + 1, ExperimentalVersion))
53+
}
54+
55+
@Test def failBumpScalaMajor: Unit = {
56+
(runTest(MajorVersion, ScalaMajorVersion + 1, ScalaMinorVersion, ExperimentalVersion))
57+
}
58+
59+
@Test def failBumpMajor: Unit = {
60+
(runTest(MajorVersion + 1, ScalaMajorVersion, ScalaMinorVersion, ExperimentalVersion))
61+
}
62+
63+
@Test def okSubtractExperimental: Unit = {
64+
(runTest(MajorVersion, ScalaMajorVersion, ScalaMinorVersion, ExperimentalVersion - 1))
65+
}
66+
67+
@Test def okSubtractScalaMinor: Unit = {
68+
(runTest(MajorVersion, ScalaMajorVersion, ScalaMinorVersion - 1, ExperimentalVersion))
69+
}
70+
71+
@Test def okSubtractScalaMajor: Unit = {
72+
(runTest(MajorVersion, ScalaMajorVersion - 1, ScalaMinorVersion, ExperimentalVersion))
73+
}
74+
75+
@Test def failSubtractMajor: Unit = {
76+
(runTest(MajorVersion - 1, ScalaMajorVersion, ScalaMinorVersion, ExperimentalVersion))
77+
}
78+
79+
80+
}

0 commit comments

Comments
 (0)