Skip to content

Commit d8a728e

Browse files
committed
Fix scala#2185: Add bytecode idempotency checks.
1 parent 8815b40 commit d8a728e

File tree

2 files changed

+110
-25
lines changed

2 files changed

+110
-25
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Decorators._
1414
class TastyPickler {
1515

1616
private val sections = new mutable.ArrayBuffer[(NameRef, TastyBuffer)]
17-
val uuid = UUID.randomUUID()
17+
private val uuid = UUID.fromString("3cee1b79-c03a-4125-b337-d067b5cb3a94") // TODO: use a hash of the tasty tree
1818

1919
private val headerBuffer = {
2020
val buf = new TastyBuffer(24)

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

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

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

265350
object CompilationTests {

0 commit comments

Comments
 (0)