diff --git a/.gitignore b/.gitignore index 11f3d71e8025..b8e3c9e0f96e 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,5 @@ compiler/test/debug/Gen.jar compiler/before-pickling.txt compiler/after-pickling.txt *.dotty-ide-version + +*.decompiled.out diff --git a/compiler/src/dotty/tools/dotc/decompiler/DecompilationPrinter.scala b/compiler/src/dotty/tools/dotc/decompiler/DecompilationPrinter.scala index b189b44133f5..9b7e6e7d1568 100644 --- a/compiler/src/dotty/tools/dotc/decompiler/DecompilationPrinter.scala +++ b/compiler/src/dotty/tools/dotc/decompiler/DecompilationPrinter.scala @@ -1,9 +1,12 @@ package dotty.tools.dotc package decompiler +import java.io.{OutputStream, PrintStream} + import dotty.tools.dotc.core.Contexts._ import dotty.tools.dotc.core.Phases.Phase import dotty.tools.dotc.core.tasty.TastyPrinter +import dotty.tools.io.{File, Path} /** Phase that prints the trees in all loaded compilation units. * @@ -14,23 +17,39 @@ class DecompilationPrinter extends Phase { override def phaseName: String = "decompilationPrinter" override def run(implicit ctx: Context): Unit = { - val unit = ctx.compilationUnit + val outputDir = ctx.settings.outputDir.value + if (outputDir == ".") printToOutput(System.out) + else { + var os: OutputStream = null + var ps: PrintStream = null + try { + os = File(outputDir + ".decompiled").outputStream() + ps = new PrintStream(os) + printToOutput(ps) + } finally { + if (os ne null) os.close() + if (ps ne null) ps.close() + } + } + } + private def printToOutput(out: PrintStream)(implicit ctx: Context): Unit = { + val unit = ctx.compilationUnit val pageWidth = ctx.settings.pageWidth.value val doubleLine = "=" * pageWidth val line = "-" * pageWidth - println(doubleLine) - println(unit.source) - println(line) + out.println(doubleLine) + out.println(unit.source) + out.println(line) - println(unit.tpdTree.show) - println(line) + out.println(unit.tpdTree.show) + out.println(line) if (ctx.settings.printTasty.value) { new TastyPrinter(unit.pickled.head._2).printContents() - println(line) + out.println(line) } } } diff --git a/compiler/test/dotty/tools/dotc/FromTastyTests.scala b/compiler/test/dotty/tools/dotc/FromTastyTests.scala index 91617aa027f0..caddc61caf1a 100644 --- a/compiler/test/dotty/tools/dotc/FromTastyTests.scala +++ b/compiler/test/dotty/tools/dotc/FromTastyTests.scala @@ -26,7 +26,7 @@ class FromTastyTests extends ParallelTesting { // > dotc -Ythrough-tasty -Ycheck:all implicit val testGroup: TestGroup = TestGroup("posTestFromTasty") - val (step1, step2) = compileTastyInDir("../tests/pos", defaultOptions, + val (step1, step2, step3) = compileTastyInDir("../tests/pos", defaultOptions, blacklist = Set( "NoCyclicReference.scala", "depfuntype.scala", @@ -53,7 +53,8 @@ class FromTastyTests extends ParallelTesting { ) step1.checkCompile() // Compile all files to generate the class files with tasty step2.checkCompile() // Compile from tasty - (step1 + step2).delete() + step3.checkCompile() // Decompile from tasty + (step1 + step2 + step3).delete() } @Test def runTestFromTasty: Unit = { @@ -63,7 +64,7 @@ class FromTastyTests extends ParallelTesting { // > dotr Test implicit val testGroup: TestGroup = TestGroup("runTestFromTasty") - val (step1, step2) = compileTastyInDir("../tests/run", defaultOptions, + val (step1, step2, step3) = compileTastyInDir("../tests/run", defaultOptions, blacklist = Set( "Course-2002-13.scala", "bridges.scala", @@ -93,7 +94,8 @@ class FromTastyTests extends ParallelTesting { ) step1.checkCompile() // Compile all files to generate the class files with tasty step2.checkRuns() // Compile from tasty and run the result - (step1 + step2).delete() + step3.checkCompile() // Decompile from tasty + (step1 + step2 + step3).delete() } private implicit class tastyCompilationTuples(tup: (CompilationTest, CompilationTest)) { diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index 062eb929e3e8..32df00e60db7 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -22,6 +22,7 @@ import dotc.reporting.diagnostic.MessageContainer import dotc.interfaces.Diagnostic.ERROR import dotc.util.DiffUtil import dotc.{ Driver, Compiler } +import dotc.decompiler /** A parallel testing suite whose goal is to integrate nicely with JUnit * @@ -133,7 +134,8 @@ trait ParallelTesting extends RunnerOrchestration { self => files: Array[JFile], flags: TestFlags, outDir: JFile, - fromTasty: Boolean = false + fromTasty: Boolean = false, + decompilation: Boolean = false ) extends TestSource { def sourceFiles: Array[JFile] = files.filter(isSourceFile) @@ -215,7 +217,7 @@ trait ParallelTesting extends RunnerOrchestration { self => private val filteredSources = if (!testFilter.isDefined) testSources else testSources.filter { - case JointCompilationSource(_, files, _, _, _) => + case JointCompilationSource(_, files, _, _, _, _) => files.exists(file => file.getAbsolutePath.contains(testFilter.get)) case SeparateCompilationSource(_, dir, _, _) => dir.getAbsolutePath.contains(testFilter.get) @@ -422,6 +424,34 @@ trait ParallelTesting extends RunnerOrchestration { self => reporter } + protected def decompile(flags0: TestFlags, suppressErrors: Boolean, targetDir: JFile): TestReporter = { + val decompilationOutput = new JFile(targetDir.getPath) + decompilationOutput.mkdir() + val flags = + flags0 and ("-d", decompilationOutput.getAbsolutePath) and + "-decompile" and "-pagewidth" and "80" + + def hasTastyFileToClassName(f: JFile): String = + targetDir.toPath.relativize(f.toPath).toString.dropRight(".hasTasty".length).replace('/', '.') + val classes = flattenFiles(targetDir).filter(isHasTastyFile).map(hasTastyFileToClassName) + + val reporter = + TestReporter.reporter(realStdout, logLevel = + if (suppressErrors || suppressAllOutput) ERROR + 1 else ERROR) + + val driver = decompiler.Main + + // Compile with a try to catch any StackTrace generated by the compiler: + try { + driver.process(flags.all ++ classes, reporter = reporter) + } + catch { + case NonFatal(ex) => reporter.logStackTrace(ex) + } + + reporter + } + private[ParallelTesting] def executeTestSuite(): this.type = { assert(_testSourcesCompleted == 0, "not allowed to re-use a `CompileRun`") @@ -474,9 +504,55 @@ trait ParallelTesting extends RunnerOrchestration { self => protected def encapsulatedCompilation(testSource: TestSource) = new LoggedRunnable { def checkTestSource(): Unit = tryCompile(testSource) { testSource match { - case testSource @ JointCompilationSource(_, files, flags, outDir, fromTasty) => + case testSource @ JointCompilationSource(name, files, flags, outDir, fromTasty, decompilation) => val reporter = - if (fromTasty) compileFromTasty(flags, false, outDir) + if (decompilation) { + val rep = decompile(flags, false, outDir) + + val checkFileOpt = files.flatMap { file => + if (file.isDirectory) Nil + else { + val fname = file.getAbsolutePath.reverse.dropWhile(_ != '.').reverse + "decompiled" + val checkFile = new JFile(fname) + if (checkFile.exists) List(checkFile) + else Nil + } + }.headOption + checkFileOpt match { + case Some(checkFile) => + val stripTrailingWhitespaces = "(.*\\S|)\\s+".r + val output = Source.fromFile(outDir + ".decompiled").getLines().map {line => + stripTrailingWhitespaces.unapplySeq(line).map(_.head).getOrElse(line) + }.mkString("\n") + .replaceFirst("@scala\\.annotation\\.internal\\.SourceFile\\([^\\)]+\\)( |\\n )", "") // FIXME: should not be printed in the decompiler + + val check: String = Source.fromFile(checkFile).getLines().mkString("\n") + + + if (output != check) { + val outFile = dotty.tools.io.File(checkFile.toPath).addExtension(".out") + outFile.writeAll(output) + val msg = + s"""Output differed for test $name, use the following command to see the diff: + | > diff $checkFile $outFile + """.stripMargin + + echo(msg) + addFailureInstruction(msg) + + // Print build instructions to file and summary: + val buildInstr = testSource.buildInstructions(0, rep.warningCount) + addFailureInstruction(buildInstr) + + // Fail target: + failTestSource(testSource) + } + case _ => + } + + rep + } + else if (fromTasty) compileFromTasty(flags, false, outDir) else compile(testSource.sourceFiles, flags, false, outDir) registerCompletion(reporter.errorCount) @@ -573,7 +649,7 @@ trait ParallelTesting extends RunnerOrchestration { self => protected def encapsulatedCompilation(testSource: TestSource) = new LoggedRunnable { def checkTestSource(): Unit = tryCompile(testSource) { val (compilerCrashed, errorCount, warningCount, verifier: Function0[Unit]) = testSource match { - case testSource @ JointCompilationSource(_, files, flags, outDir, fromTasty) => + case testSource @ JointCompilationSource(_, files, flags, outDir, fromTasty, decompilation) => val checkFile = files.flatMap { file => if (file.isDirectory) Nil else { @@ -682,7 +758,7 @@ trait ParallelTesting extends RunnerOrchestration { self => } val (compilerCrashed, expectedErrors, actualErrors, hasMissingAnnotations, errorMap) = testSource match { - case testSource @ JointCompilationSource(_, files, flags, outDir, fromTasty) => + case testSource @ JointCompilationSource(_, files, flags, outDir, fromTasty, decompilation) => val sourceFiles = testSource.sourceFiles val (errorMap, expectedErrors) = getErrorMapAndExpectedCount(sourceFiles) val reporter = compile(sourceFiles, flags, true, outDir) @@ -967,7 +1043,7 @@ trait ParallelTesting extends RunnerOrchestration { self => */ def copyToTarget(): CompilationTest = new CompilationTest ( targets.map { - case target @ JointCompilationSource(_, files, _, outDir, _) => + case target @ JointCompilationSource(_, files, _, outDir, _, _) => target.copy(files = files.map(copyToDir(outDir,_))) case target @ SeparateCompilationSource(_, dir, _, outDir) => target.copy(dir = copyToDir(outDir, dir)) @@ -1083,34 +1159,6 @@ trait ParallelTesting extends RunnerOrchestration { self => new CompilationTest(target) } - /** Compiles a single file from the string path `f` using the supplied flags - * - * Tests in the first part of the tuple must be executed before the second. - * Both testsRequires explicit delete(). - */ - def compileTasty(f: String, flags: TestFlags)(implicit testGroup: TestGroup): (CompilationTest, CompilationTest) = { - val sourceFile = new JFile(f) - val parent = sourceFile.getParentFile - val outDir = - defaultOutputDir + testGroup + "/" + - sourceFile.getName.substring(0, sourceFile.getName.lastIndexOf('.')) + "/" - - require( - sourceFile.exists && !sourceFile.isDirectory && - (parent ne null) && parent.exists && parent.isDirectory, - s"Source file: $f, didn't exist" - ) - val tastySource = createOutputDirsForFile(sourceFile, parent, outDir) - val target = JointCompilationSource( - testGroup.name, - Array(sourceFile), - flags.withClasspath(tastySource.getPath) and "-from-tasty", - tastySource, - fromTasty = true - ) - (compileFile(f, flags).keepOutput, new CompilationTest(target).keepOutput) - } - /** Compiles a directory `f` using the supplied `flags`. This method does * deep compilation, that is - it compiles all files and subdirectories * contained within the directory `f`. @@ -1216,7 +1264,7 @@ trait ParallelTesting extends RunnerOrchestration { self => * Both testsRequires explicit delete(). */ def compileTastyInDir(f: String, flags0: TestFlags, blacklist: Set[String] = Set.empty)( - implicit testGroup: TestGroup): (CompilationTest, CompilationTest) = { + implicit testGroup: TestGroup): (CompilationTest, CompilationTest, CompilationTest) = { val outDir = defaultOutputDir + testGroup + "/" val flags = flags0 and "-Yretain-trees" val sourceDir = new JFile(f) @@ -1231,9 +1279,22 @@ trait ParallelTesting extends RunnerOrchestration { self => } // TODO add SeparateCompilationSource from tasty? + val targets2 = + files + .filter(f => dotty.tools.io.File(f.toPath).changeExtension("decompiled").exists) + .map { f => + val classpath = createOutputDirsForFile(f, sourceDir, outDir) + JointCompilationSource(testGroup.name, Array(f), flags.withClasspath(classpath.getPath), classpath, decompilation = true) + } + // Create a CompilationTest and let the user decide whether to execute a pos or a neg test val generateClassFiles = compileFilesInDir(f, flags0, blacklist) - (generateClassFiles.keepOutput, new CompilationTest(targets).keepOutput) + + ( + generateClassFiles.keepOutput, + new CompilationTest(targets).keepOutput, + new CompilationTest(targets2).keepOutput + ) } diff --git a/tests/pos/lambda.decompiled b/tests/pos/lambda.decompiled new file mode 100644 index 000000000000..75e5e32589f0 --- /dev/null +++ b/tests/pos/lambda.decompiled @@ -0,0 +1,13 @@ +================================================================================ +../out/posTestFromTasty/pos/lambda/foo/Foo.class +-------------------------------------------------------------------------------- +package foo { + class Foo() extends Object() { + val a: Int => Int = + { + def $anonfun(x: Int): Int = x.*(x) + closure($anonfun) + } + } +} +-------------------------------------------------------------------------------- diff --git a/tests/pos/lambda.scala b/tests/pos/lambda.scala new file mode 100644 index 000000000000..952e28826894 --- /dev/null +++ b/tests/pos/lambda.scala @@ -0,0 +1,4 @@ +package foo +class Foo { + val a = (x: Int) => x * x +} diff --git a/tests/pos/methodTypes.decompiled b/tests/pos/methodTypes.decompiled new file mode 100644 index 000000000000..776508a9e5cd --- /dev/null +++ b/tests/pos/methodTypes.decompiled @@ -0,0 +1,11 @@ +================================================================================ +../out/posTestFromTasty/pos/methodTypes/Foo.class +-------------------------------------------------------------------------------- +package { + class Foo() extends Object() { + val x: Int = 1 + def y: Int = 2 + def z(): Int = 3 + } +} +-------------------------------------------------------------------------------- \ No newline at end of file diff --git a/tests/pos/methodTypes.scala b/tests/pos/methodTypes.scala new file mode 100644 index 000000000000..be509a0e5c30 --- /dev/null +++ b/tests/pos/methodTypes.scala @@ -0,0 +1,5 @@ +class Foo { + val x = 1 + def y = 2 + def z() = 3 +} diff --git a/tests/pos/simpleCaseObject.decompiled b/tests/pos/simpleCaseObject.decompiled new file mode 100644 index 000000000000..35761b4ac5d7 --- /dev/null +++ b/tests/pos/simpleCaseObject.decompiled @@ -0,0 +1,21 @@ +================================================================================ +../out/posTestFromTasty/pos/simpleCaseObject/foo/Foo.class +-------------------------------------------------------------------------------- +package foo { + final lazy module case val Foo: foo.Foo = new foo.Foo() + final module case class Foo() extends Object() with _root_.scala.Product { + this: foo.Foo.type => + + override def hashCode(): Int = 1045991777 + override def toString(): String = "Foo" + override def canEqual(that: Any): Boolean = that.isInstanceOf[foo.Foo] + override def productArity: Int = 0 + override def productPrefix: String = "Foo" + override def productElement(n: Int): Any = + n match + { + case _ => throw new IndexOutOfBoundsException(n.toString()) + } + } +} +-------------------------------------------------------------------------------- \ No newline at end of file diff --git a/tests/pos/simpleClass.decompiled b/tests/pos/simpleClass.decompiled new file mode 100644 index 000000000000..86b7cf6b1204 --- /dev/null +++ b/tests/pos/simpleClass.decompiled @@ -0,0 +1,7 @@ +================================================================================ +../out/posTestFromTasty/pos/simpleClass/foo/A.class +-------------------------------------------------------------------------------- +package foo { + class A() extends Object() {} +} +-------------------------------------------------------------------------------- \ No newline at end of file diff --git a/tests/run/puzzle.decompiled b/tests/run/puzzle.decompiled new file mode 100644 index 000000000000..c578bead0631 --- /dev/null +++ b/tests/run/puzzle.decompiled @@ -0,0 +1,18 @@ +================================================================================ +../out/runTestFromTasty/run/puzzle/Test.class +-------------------------------------------------------------------------------- +package { + final lazy module val Test: Test = new Test() + final module class Test() extends Object() { this: Test.type => + def main(args: Array[String]): Unit = + { + println(if false then 5.0 else 53.0) + val x: Double = if false then 5.0 else 53.0 + println(x) + val z: Long = 1L + val y: Float = Long.long2float(z) + () + } + } +} +-------------------------------------------------------------------------------- \ No newline at end of file