Skip to content

Commit b152c19

Browse files
committed
WIP: Try new algorithm for validation
1 parent 99fe058 commit b152c19

File tree

4 files changed

+202
-83
lines changed

4 files changed

+202
-83
lines changed

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

Lines changed: 11 additions & 4 deletions
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)]
@@ -36,16 +45,14 @@ class TastyPickler(val rootCls: ClassSymbol) {
3645
// Hash of positions, comments and any additional section
3746
val uuidHi: Long = otherSectionHashes.fold(0L)(_ ^ _)
3847

39-
val compilerBytes = config.Properties.versionString.getBytes(java.nio.charset.StandardCharsets.UTF_8)
40-
4148
val headerBuffer = {
4249
val buf = new TastyBuffer(header.length + 32)
4350
for (ch <- header) buf.writeByte(ch.toByte)
4451
buf.writeNat(MajorVersion)
4552
buf.writeNat(MinorVersion)
4653
buf.writeNat(ExperimentalVersion)
47-
buf.writeNat(compilerBytes.length)
48-
buf.writeBytes(compilerBytes, compilerBytes.length)
54+
buf.writeNat(TastyPickler.versionStringBytes.length)
55+
buf.writeBytes(TastyPickler.versionStringBytes, TastyPickler.versionStringBytes.length)
4956
buf.writeUncompressedLong(uuidLow)
5057
buf.writeUncompressedLong(uuidHi)
5158
buf

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

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ sealed abstract case class TastyHeader(
2424
majorVersion: Int,
2525
minorVersion: Int,
2626
experimentalVersion: Int,
27-
compilerVersion: String
27+
compilerVersion: String // String could lead to a lot of duplication
2828
)
2929

3030
class TastyHeaderUnpickler(reader: TastyReader) {
@@ -46,8 +46,6 @@ class TastyHeaderUnpickler(reader: TastyReader) {
4646

4747
def expectedMinor = showMinorVersion(MinorVersion, ExperimentalVersion)
4848

49-
def myScalaV = "<???.???.???>"
50-
5149
def myAddendum = {
5250
if (ExperimentalVersion > 0)
5351
"\nNote that you are currently using an unstable compiler version."
@@ -57,88 +55,78 @@ class TastyHeaderUnpickler(reader: TastyReader) {
5755

5856
for (i <- 0 until header.length)
5957
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()
58+
val fileMajor = readNat()
59+
if (fileMajor <= 27) { // old behavior before Scala 3.0.0-M4
60+
val fileMinor = readNat()
6361
throw new UnpickleException(
6462
s"""TASTy signature is from a backwards incompatible release.
6563
| expected: {majorVersion: $MajorVersion, minorVersion: $expectedMinor}
66-
| found : {majorVersion: $majorVersion, minorVersion: $minorVersion}
64+
| found : {majorVersion: $fileMajor, minorVersion: $fileMinor}
6765
|
68-
|Please recompile this TASTy with at least Scala $myScalaV.$myAddendum""".stripMargin
66+
|Please recompile this TASTy with a later Scala version.$myAddendum""".stripMargin
6967
)
7068
}
7169
else {
72-
check(majorVersion <= MajorVersion, // layout of header may have changed in future release
70+
check(fileMajor <= MajorVersion, // layout of header may have changed in future release
7371
s"""TASTy signature is from a future release of Scala with unknown TASTy format.
7472
| expected: {majorVersion: $MajorVersion, minorVersion: $expectedMinor}
75-
| found : {majorVersion: $majorVersion, ...}
73+
| found : {majorVersion: $fileMajor, ...}
7674
|
77-
|Please use TASTy compiled by Scala $myScalaV.$myAddendum""".stripMargin)
78-
val minorVersion = readNat()
79-
val experimentalVersion = readNat()
75+
|Please use TASTy compiled an earlier Scala version.$myAddendum""".stripMargin)
76+
val fileMinor = readNat()
77+
val fileExperimental = readNat()
8078
val compilerVersion = {
8179
val length = readNat()
8280
val start = currentAddr
8381
val end = start + length
8482
goto(end)
85-
new String(bytes, start.index, length)
83+
new String(bytes, start.index, length) // NOTE: new string object here (should probably cache as it leaks)
8684
}
87-
// |=============|==============|========|
88-
// | Our Version | Read Version | Valid |
89-
// |=============|==============|========|
90-
// | 28. | 27. | false |
91-
// | 28. | 29. | false |
92-
// | 28.0 | 28.1 | false |
93-
// | 28.1 | 28.0 | true |
94-
// | 28.0 @ 0 | 28.0 @ 0 | true |
95-
// | 28.0 @ 0 | 28.0 @ 1 | false | // implementation could have changed between 0 and 1
96-
// | 28.0 @ 1 | 28.0 @ 0 | false | // implementation could have changed between 1 and 0
97-
// | 28.0 @ 1 | 28.0 @ 2 | false | // implementation could have changed between 1 and 2
98-
// | 28.0 @ 2 | 28.0 @ 1 | false | // implementation could have changed between 2 and 1
99-
// |=============|==============|========|
100-
101-
// Example series:
102-
// Scala 3.0.0-M4 - 28.0 @ 1
103-
// Scala 3.0.0-RC1 - 28.0 @ 1
104-
// Scala 3.0.0 - 28.0 @ 0 // reset experimental to 0 for final version
105-
// Scala 3.0.1 - 28.0 @ 0
106-
// Scala 3.0.2 - 28.0 @ 0
107-
// Scala 3.1.0-RC1 - 28.1 @ 1 // we intend to break forward compat, bump both minor and experimental
108-
// Scala 3.1.0-RC2 - 28.1 @ 2 // we try another change, bump only experimental, should (`28.1 @ 1` be able to read `28.1 @ 2`?)
109-
// Scala 3.1.0 - 28.1 @ 0 // reset experimental to 0 for final version
11085

111-
val validVersion = (
112-
majorVersion == MajorVersion
113-
&& minorVersion <= MinorVersion
114-
&& !(minorVersion == MinorVersion && ExperimentalVersion != experimentalVersion) // experimentals are forks
86+
val validVersion = ( // inlined from `dotty.tools.tasty.TastyVersionFormatTest.TastyVersion.<:<`
87+
fileMajor == MajorVersion && (
88+
if (fileExperimental == ExperimentalVersion) {
89+
if (ExperimentalVersion == 0) {
90+
fileMinor <= MinorVersion
91+
}
92+
else {
93+
fileMinor == MinorVersion
94+
}
95+
}
96+
else {
97+
fileExperimental == 0 && fileMinor < MinorVersion
98+
}
99+
)
115100
)
116101
check(validVersion, {
117-
val foundMinor = showMinorVersion(minorVersion, experimentalVersion)
102+
val foundMinor = showMinorVersion(fileMinor, fileExperimental)
118103
val foundAddendum = {
119-
if (experimentalVersion > 0)
104+
if (fileExperimental > 0)
120105
"\nNote that this TASTy file was produced by an unstable compiler version."
121106
else
122107
""
123108
}
124-
if (majorVersion < MajorVersion) {
109+
if (fileMajor < MajorVersion) {
125110
s"""TASTy signature is from a backwards incompatible release.
126111
| expected: {majorVersion: $MajorVersion, minorVersion: $expectedMinor}
127-
| found : {majorVersion: $majorVersion, minorVersion: $foundMinor}
112+
| found : {majorVersion: $fileMajor, minorVersion: $foundMinor}
128113
|
129-
|Please recompile this TASTy with at least Scala $myScalaV.$myAddendum$foundAddendum""".stripMargin
114+
|Please recompile this TASTy with a later Scala version.
115+
|The TASTy file was compiled by $compilerVersion.$myAddendum$foundAddendum""".stripMargin
130116
}
131117
else {
118+
// TODO ajust message based on experimental vs forward incompat
132119
s"""TASTy signature is from a forwards incompatible release.
133120
| expected: {majorVersion: $MajorVersion, minorVersion: $expectedMinor}
134-
| found : {majorVersion: $majorVersion, minorVersion: $foundMinor}
121+
| found : {majorVersion: $fileMajor, minorVersion: $foundMinor}
135122
|
136-
|To read this TASTy file, please upgrade your Scala version to at least $compilerVersion.$myAddendum$foundAddendum""".stripMargin
123+
|To read this TASTy file, please upgrade your Scala version.
124+
|The TASTy file was compiled by $compilerVersion.$myAddendum$foundAddendum""".stripMargin
137125
}
138126
}
139127
)
140128
val uuid = new UUID(readUncompressedLong(), readUncompressedLong())
141-
new TastyHeader(uuid, majorVersion, minorVersion, experimentalVersion, compilerVersion) {}
129+
new TastyHeader(uuid, fileMajor, fileMinor, fileExperimental, compilerVersion) {}
142130
}
143131
}
144132

tasty/test/dotty/tools/tasty/TastyHeaderUnpicklerTest.scala

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,44 @@ import org.junit.{Test, Ignore}
66
import TastyFormat._
77
import TastyBuffer._
88

9-
// @Ignore // comment if you want to experiment with error messages
9+
@Ignore // comment if you want to experiment with error messages
1010
class TastyHeaderUnpicklerTest {
1111

12+
import TastyHeaderUnpicklerTest._
13+
14+
@Test def vanilla: Unit = {
15+
runTest(MajorVersion, MinorVersion, ExperimentalVersion, "3.0.0-M4-bin-SNAPSHOT-git-12345")
16+
}
17+
18+
@Test def failBumpExperimental: Unit = {
19+
(runTest(MajorVersion, MinorVersion, ExperimentalVersion + 1, "3.0.0-M4-bin-SNAPSHOT-git-12345"))
20+
}
21+
22+
@Test def failBumpMinor: Unit = {
23+
(runTest(MajorVersion, MinorVersion + 1, ExperimentalVersion, "3.0.0-M4-bin-SNAPSHOT-git-12345"))
24+
}
25+
26+
@Test def failBumpMajor: Unit = {
27+
(runTest(MajorVersion + 1, MinorVersion, ExperimentalVersion, "3.0.0-M4-bin-SNAPSHOT-git-12345"))
28+
}
29+
30+
@Test def okSubtractExperimental: Unit = {
31+
(runTest(MajorVersion, MinorVersion, ExperimentalVersion - 1, "3.0.0-M4-bin-SNAPSHOT-git-12345"))
32+
}
33+
34+
@Test def okSubtractMinor: Unit = {
35+
(runTest(MajorVersion, MinorVersion - 1, ExperimentalVersion, "3.0.0-M4-bin-SNAPSHOT-git-12345"))
36+
}
37+
38+
@Test def failSubtractMajor: Unit = {
39+
(runTest(MajorVersion - 1, MinorVersion, ExperimentalVersion, "3.0.0-M4-bin-SNAPSHOT-git-12345"))
40+
}
41+
42+
}
43+
44+
object TastyHeaderUnpicklerTest {
45+
46+
1247
def fillHeader(maj: Int, min: Int, exp: Int, compiler: String): TastyBuffer = {
1348
val compilerBytes = compiler.getBytes(java.nio.charset.StandardCharsets.UTF_8)
1449
val buf = new TastyBuffer(header.length + 32 + compilerBytes.length)
@@ -42,33 +77,4 @@ class TastyHeaderUnpicklerTest {
4277
}
4378
}
4479

45-
@Test def vanilla: Unit = {
46-
runTest(MajorVersion, MinorVersion, ExperimentalVersion, "version 3.0.0-M4-bin-SNAPSHOT-git-12345")
47-
}
48-
49-
@Test def failBumpExperimental: Unit = {
50-
(runTest(MajorVersion, MinorVersion, ExperimentalVersion + 1, "version 3.0.0-M4-bin-SNAPSHOT-git-12345"))
51-
}
52-
53-
@Test def failBumpMinor: Unit = {
54-
(runTest(MajorVersion, MinorVersion + 1, ExperimentalVersion, "version 3.0.0-M4-bin-SNAPSHOT-git-12345"))
55-
}
56-
57-
@Test def failBumpMajor: Unit = {
58-
(runTest(MajorVersion + 1, MinorVersion, ExperimentalVersion, "version 3.0.0-M4-bin-SNAPSHOT-git-12345"))
59-
}
60-
61-
@Test def okSubtractExperimental: Unit = {
62-
(runTest(MajorVersion, MinorVersion, ExperimentalVersion - 1, "version 3.0.0-M4-bin-SNAPSHOT-git-12345"))
63-
}
64-
65-
@Test def okSubtractMinor: Unit = {
66-
(runTest(MajorVersion, MinorVersion - 1, ExperimentalVersion, "version 3.0.0-M4-bin-SNAPSHOT-git-12345"))
67-
}
68-
69-
@Test def failSubtractMajor: Unit = {
70-
(runTest(MajorVersion - 1, MinorVersion, ExperimentalVersion, "version 3.0.0-M4-bin-SNAPSHOT-git-12345"))
71-
}
72-
73-
7480
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
class TastyVersionFormatTest {
10+
11+
import TastyVersionFormatTest._
12+
13+
/** aliases `TastyVersion.apply` */
14+
def compiler(major: Int, minor: Int, experimental: Int) = TastyVersion(major, minor, experimental)
15+
16+
/** aliases `TastyVersion.apply` */
17+
def file(major: Int, minor: Int, experimental: Int) = TastyVersion(major, minor, experimental)
18+
19+
@Test def accept_ExperimentalReadEQExperimental_EQMinor: Unit = {
20+
assert(file(28,1,1) <:< compiler(28,1,1)) // same minor, same experimental
21+
}
22+
23+
@Test def accept_ExperimentalReadFinal_LTMinor: Unit = {
24+
assert(file(28,0,0) <:< compiler(28,1,1)) // proceeding minor
25+
}
26+
27+
@Test def accept_FinalReadFinal_LTEqualMinor: Unit = {
28+
assert(file(28,0,0) <:< compiler(28,1,0)) // proceeding minor
29+
assert(file(28,0,0) <:< compiler(28,0,0)) // same minor
30+
}
31+
32+
/** these cases are unrelated because a final compiler can only read final tasty of <= minor version */
33+
@Test def reject_FinalReadFinal_GTMinor: Unit = {
34+
assert(file(28,2,0) unrelatedTo compiler(28,1,0)) // succeeding minor
35+
}
36+
37+
/** these cases are unrelated because a final compiler can not read experimental tasty */
38+
@Test def reject_FinalReadExperimental: Unit = {
39+
assert(file(28,0,1) unrelatedTo compiler(28,1,0)) // proceeding minor
40+
assert(file(28,1,1) unrelatedTo compiler(28,1,0)) // same minor
41+
assert(file(28,2,1) unrelatedTo compiler(28,1,0)) // succeeding minor
42+
}
43+
44+
/** These cases are unrelated because an experimental compiler can only read final tasty of < minor version */
45+
@Test def reject_ExperimentalReadFinal_GTEqualMinor: Unit = {
46+
assert(file(28,2,0) unrelatedTo compiler(28,1,1)) // succeeding minor
47+
assert(file(28,1,0) unrelatedTo compiler(28,1,1)) // equal minor
48+
}
49+
50+
/**These cases are unrelated because both compiler and file are experimental,
51+
* and with unequal experimental part.
52+
*/
53+
@Test def reject_ExperimentalReadNEExperimental: Unit = {
54+
assert(file(28,1,2) unrelatedTo compiler(28,1,1)) // same minor version, succeeding experimental
55+
assert(file(28,1,1) unrelatedTo compiler(28,1,2)) // same minor version, proceeding experimental
56+
}
57+
58+
/** these cases are unrelated because the major version must be identical */
59+
@Test def reject_NEMajor: Unit = {
60+
assert(file(27,0,0) unrelatedTo compiler(28,0,0)) // less than
61+
assert(file(29,0,0) unrelatedTo compiler(28,0,0)) // greater than
62+
}
63+
64+
}
65+
66+
object TastyVersionFormatTest {
67+
68+
case class TastyVersion(major: Int, minor: Int, experimental: Int) { file =>
69+
70+
/**if `file <:< compiler` then tasty file is valid to be read.
71+
*
72+
* Follows the given algorithm:
73+
* ```
74+
* if file.major != compiler.major then
75+
* return incompatible
76+
* if compiler.experimental == 0 then
77+
* if file.experimental != 0 then
78+
* return incompatible
79+
* if file.minor > compiler.minor then
80+
* return incompatible
81+
* else
82+
* return compatible
83+
* else invariant[compiler.experimental != 0]
84+
* if file.experimental == compiler.experimental then
85+
* if file.minor == compiler.minor then
86+
* return compatible (all fields equal)
87+
* else
88+
* return incompatible
89+
* else if file.experimental == 0,
90+
* if file.minor < compiler.minor then
91+
* return compatible (an experimental version can read a previous released version)
92+
* else
93+
* return incompatible (an experimental version cannot read its own minor version or any later version)
94+
* else invariant[file.experimental is non-0 and different than compiler.experimental]
95+
* return incompatible
96+
* ```
97+
*/
98+
def <:<(compiler: TastyVersion): Boolean = (
99+
file.major == compiler.major && (
100+
if (file.experimental == compiler.experimental) {
101+
if (compiler.experimental == 0) {
102+
file.minor <= compiler.minor
103+
}
104+
else {
105+
file.minor == compiler.minor
106+
}
107+
}
108+
else {
109+
file.experimental == 0 && file.minor < compiler.minor
110+
}
111+
)
112+
)
113+
114+
/**if `file unrelated compiler` then tasty file must be rejected.*/
115+
def unrelatedTo(compiler: TastyVersion): Boolean = !(file <:< compiler)
116+
}
117+
118+
}

0 commit comments

Comments
 (0)