Skip to content

Add decomplilation tests #3701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ compiler/test/debug/Gen.jar
compiler/before-pickling.txt
compiler/after-pickling.txt
*.dotty-ide-version

*.decompiled.out
Original file line number Diff line number Diff line change
@@ -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.
*
Expand All @@ -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)
}
}
}
10 changes: 6 additions & 4 deletions compiler/test/dotty/tools/dotc/FromTastyTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class FromTastyTests extends ParallelTesting {
// > dotc -Ythrough-tasty -Ycheck:all <source>

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",
Expand All @@ -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 = {
Expand All @@ -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",
Expand Down Expand Up @@ -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)) {
Expand Down
135 changes: 98 additions & 37 deletions compiler/test/dotty/tools/vulpix/ParallelTesting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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`")

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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)
Expand All @@ -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
)
}


Expand Down
13 changes: 13 additions & 0 deletions tests/pos/lambda.decompiled
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
--------------------------------------------------------------------------------
4 changes: 4 additions & 0 deletions tests/pos/lambda.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package foo
class Foo {
val a = (x: Int) => x * x
}
11 changes: 11 additions & 0 deletions tests/pos/methodTypes.decompiled
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
================================================================================
../out/posTestFromTasty/pos/methodTypes/Foo.class
--------------------------------------------------------------------------------
package <empty> {
class Foo() extends Object() {
val x: Int = 1
def y: Int = 2
def z(): Int = 3
}
}
--------------------------------------------------------------------------------
5 changes: 5 additions & 0 deletions tests/pos/methodTypes.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Foo {
val x = 1
def y = 2
def z() = 3
}
21 changes: 21 additions & 0 deletions tests/pos/simpleCaseObject.decompiled
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
--------------------------------------------------------------------------------
7 changes: 7 additions & 0 deletions tests/pos/simpleClass.decompiled
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
================================================================================
../out/posTestFromTasty/pos/simpleClass/foo/A.class
--------------------------------------------------------------------------------
package foo {
class A() extends Object() {}
}
--------------------------------------------------------------------------------
Loading