Skip to content

Commit f28c984

Browse files
authored
Merge pull request #2272 from dotty-staging/idempotency-checks
Fix #2185: Add bytecode idempotency checks.
2 parents 92fe2a5 + 000b432 commit f28c984

File tree

2 files changed

+144
-36
lines changed

2 files changed

+144
-36
lines changed

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

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,13 @@ package tasty
66
import TastyFormat._
77
import collection.mutable
88
import TastyBuffer._
9-
import java.util.UUID
109
import core.Symbols.Symbol
1110
import ast.tpd
1211
import Decorators._
1312

1413
class TastyPickler {
1514

1615
private val sections = new mutable.ArrayBuffer[(NameRef, TastyBuffer)]
17-
val uuid = UUID.randomUUID()
18-
19-
private val headerBuffer = {
20-
val buf = new TastyBuffer(24)
21-
for (ch <- header) buf.writeByte(ch.toByte)
22-
buf.writeNat(MajorVersion)
23-
buf.writeNat(MinorVersion)
24-
buf.writeUncompressedLong(uuid.getMostSignificantBits)
25-
buf.writeUncompressedLong(uuid.getLeastSignificantBits)
26-
buf
27-
}
2816

2917
val nameBuffer = new NameBuffer
3018

@@ -36,6 +24,20 @@ class TastyPickler {
3624
buf.assemble()
3725
buf.length + natSize(buf.length)
3826
}
27+
28+
val uuidLow: Long = pjwHash64(nameBuffer.bytes)
29+
val uuidHi: Long = sections.iterator.map(x => pjwHash64(x._2.bytes)).fold(0L)(_ ^ _)
30+
31+
val headerBuffer = {
32+
val buf = new TastyBuffer(header.length + 24)
33+
for (ch <- header) buf.writeByte(ch.toByte)
34+
buf.writeNat(MajorVersion)
35+
buf.writeNat(MinorVersion)
36+
buf.writeUncompressedLong(uuidLow)
37+
buf.writeUncompressedLong(uuidHi)
38+
buf
39+
}
40+
3941
val totalSize =
4042
headerBuffer.length +
4143
lengthWithLength(nameBuffer) + {
@@ -69,4 +71,23 @@ class TastyPickler {
6971
var addrOfSym: Symbol => Option[Addr] = (_ => None)
7072

7173
val treePkl = new TreePickler(this)
74+
75+
/** Returns a non-cryptographic 64-bit hash of the array.
76+
*
77+
* from https://en.wikipedia.org/wiki/PJW_hash_function#Implementation
78+
*/
79+
private def pjwHash64(data: Array[Byte]): Long = {
80+
var h = 0L
81+
var high = 0L
82+
var i = 0
83+
while (i < data.length) {
84+
h = (h << 4) + data(i)
85+
high = h & 0xF0000000L
86+
if (high != 0)
87+
h ^= high >> 24
88+
h &= ~high
89+
i += 1
90+
}
91+
h
92+
}
7293
}

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

Lines changed: 111 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -207,30 +207,6 @@ class CompilationTests extends ParallelTesting {
207207
compileDir("../library/src",
208208
allowDeepSubtypes.and("-Ycheck-reentrant", "-strict", "-priorityclasspath", defaultOutputDir))
209209

210-
def sources(paths: JStream[Path], excludedFiles: List[String] = Nil): List[String] =
211-
paths.iterator().asScala
212-
.filter(path =>
213-
(path.toString.endsWith(".scala") || path.toString.endsWith(".java"))
214-
&& !excludedFiles.contains(path.getFileName.toString))
215-
.map(_.toString).toList
216-
217-
val compilerDir = Paths.get("../compiler/src")
218-
val compilerSources = sources(Files.walk(compilerDir))
219-
220-
val backendDir = Paths.get("../scala-backend/src/compiler/scala/tools/nsc/backend")
221-
val backendJvmDir = Paths.get("../scala-backend/src/compiler/scala/tools/nsc/backend/jvm")
222-
223-
// NOTE: Keep these exclusions synchronized with the ones in the sbt build (Build.scala)
224-
val backendExcluded =
225-
List("JavaPlatform.scala", "Platform.scala", "ScalaPrimitives.scala")
226-
val backendJvmExcluded =
227-
List("BCodeICodeCommon.scala", "GenASM.scala", "GenBCode.scala", "ScalacBackendInterface.scala")
228-
229-
val backendSources =
230-
sources(Files.list(backendDir), excludedFiles = backendExcluded)
231-
val backendJvmSources =
232-
sources(Files.list(backendJvmDir), excludedFiles = backendJvmExcluded)
233-
234210
def dotty1 = {
235211
compileList(
236212
"dotty1",
@@ -261,6 +237,117 @@ class CompilationTests extends ParallelTesting {
261237
} :: Nil
262238
}.map(_.checkCompile()).foreach(_.delete())
263239
}
240+
241+
@Test def bytecodeIdemporency: Unit = {
242+
var failed = 0
243+
var total = 0
244+
val blacklisted = Set(
245+
// Bridges on collections in different order. Second one in scala2 order.
246+
"pos/Map/scala/collection/immutable/Map",
247+
"pos/Map/scala/collection/immutable/AbstractMap",
248+
"pos/t1203a/NodeSeq",
249+
"pos/i2345/Whatever"
250+
)
251+
def checkIdempotency(): Unit = {
252+
val groupedBytecodeFiles: List[(Path, Path, Path, Path)] = {
253+
val bytecodeFiles = {
254+
def bytecodeFiles(paths: JStream[Path]): List[Path] = {
255+
def isBytecode(file: String) = file.endsWith(".class") || file.endsWith(".tasty")
256+
paths.iterator.asScala.filter(path => isBytecode(path.toString)).toList
257+
}
258+
val compilerDir1 = Paths.get("../out/idempotency1")
259+
val compilerDir2 = Paths.get("../out/idempotency2")
260+
bytecodeFiles(Files.walk(compilerDir1)) ++ bytecodeFiles(Files.walk(compilerDir2))
261+
}
262+
val groups = bytecodeFiles.groupBy(f => f.toString.substring("../out/idempotencyN/".length, f.toString.length - 6))
263+
groups.filterNot(x => blacklisted(x._1)).valuesIterator.flatMap { g =>
264+
def pred(f: Path, i: Int, isTasty: Boolean) =
265+
f.toString.contains("idempotency" + i) && f.toString.endsWith(if (isTasty) ".tasty" else ".class")
266+
val class1 = g.find(f => pred(f, 1, isTasty = false))
267+
val class2 = g.find(f => pred(f, 2, isTasty = false))
268+
val tasty1 = g.find(f => pred(f, 1, isTasty = true))
269+
val tasty2 = g.find(f => pred(f, 2, isTasty = true))
270+
assert(class1.isDefined, "Could not find class in idempotency1 for " + class2)
271+
assert(class2.isDefined, "Could not find class in idempotency2 for " + class1)
272+
if (tasty1.isEmpty || tasty2.isEmpty) Nil
273+
else List(Tuple4(class1.get, tasty1.get, class2.get, tasty2.get))
274+
}.toList
275+
}
276+
277+
for ((class1, tasty1, class2, tasty2) <- groupedBytecodeFiles) {
278+
total += 1
279+
val bytes1 = Files.readAllBytes(class1)
280+
val bytes2 = Files.readAllBytes(class2)
281+
if (!java.util.Arrays.equals(bytes1, bytes2)) {
282+
failed += 1
283+
val tastyBytes1 = Files.readAllBytes(tasty1)
284+
val tastyBytes2 = Files.readAllBytes(tasty2)
285+
if (java.util.Arrays.equals(tastyBytes1, tastyBytes2))
286+
println(s"Idempotency test failed between $class1 and $class1 (same tasty)")
287+
else
288+
println(s"Idempotency test failed between $tasty1 and $tasty2")
289+
/* Dump bytes to console, could be useful if issue only appears in CI.
290+
* Create the .class locally with Files.write(path, Array[Byte](...)) with the printed array
291+
*/
292+
// println(bytes1.mkString("Array[Byte](", ",", ")"))
293+
// println(bytes2.mkString("Array[Byte](", ",", ")"))
294+
}
295+
}
296+
}
297+
298+
val opt = defaultOptions.and("-YemitTasty")
299+
300+
def idempotency1() = {
301+
compileList("dotty1", compilerSources ++ backendSources ++ backendJvmSources, opt) +
302+
compileFilesInDir("../tests/pos", opt)
303+
}
304+
def idempotency2() = {
305+
compileList("dotty1", compilerSources ++ backendSources ++ backendJvmSources, opt) +
306+
compileFilesInDir("../tests/pos", opt)
307+
}
308+
309+
val tests = (idempotency1() + idempotency2()).keepOutput.checkCompile()
310+
311+
assert(new java.io.File("../out/idempotency1/").exists)
312+
assert(new java.io.File("../out/idempotency2/").exists)
313+
314+
val t0 = System.currentTimeMillis()
315+
checkIdempotency()
316+
println(s"checked bytecode idempotency (${(System.currentTimeMillis() - t0) / 1000.0} sec)")
317+
318+
tests.delete()
319+
320+
assert(failed == 0, s"Failed $failed idempotency checks (out of $total)")
321+
}
322+
323+
324+
private val (compilerSources, backendSources, backendJvmSources) = {
325+
def sources(paths: JStream[Path], excludedFiles: List[String] = Nil): List[String] =
326+
paths.iterator().asScala
327+
.filter(path =>
328+
(path.toString.endsWith(".scala") || path.toString.endsWith(".java"))
329+
&& !excludedFiles.contains(path.getFileName.toString))
330+
.map(_.toString).toList
331+
332+
val compilerDir = Paths.get("../compiler/src")
333+
val compilerSources0 = sources(Files.walk(compilerDir))
334+
335+
val backendDir = Paths.get("../scala-backend/src/compiler/scala/tools/nsc/backend")
336+
val backendJvmDir = Paths.get("../scala-backend/src/compiler/scala/tools/nsc/backend/jvm")
337+
338+
// NOTE: Keep these exclusions synchronized with the ones in the sbt build (Build.scala)
339+
val backendExcluded =
340+
List("JavaPlatform.scala", "Platform.scala", "ScalaPrimitives.scala")
341+
val backendJvmExcluded =
342+
List("BCodeICodeCommon.scala", "GenASM.scala", "GenBCode.scala", "ScalacBackendInterface.scala")
343+
344+
val backendSources0 =
345+
sources(Files.list(backendDir), excludedFiles = backendExcluded)
346+
val backendJvmSources0 =
347+
sources(Files.list(backendJvmDir), excludedFiles = backendJvmExcluded)
348+
349+
(compilerSources0, backendSources0, backendJvmSources0)
350+
}
264351
}
265352

266353
object CompilationTests {

0 commit comments

Comments
 (0)