Repository: mattgodbolt/compiler-explorer
Pull request #1396 (2019-05-26 17:48:47)

Java support
![image](https://user-images.githubusercontent.com/349397/58385239-7ba68c80-7fed-11e9-830c-90f4e527acda.png)

This adds Java support to compiler explorer by compiling through `javac` and using `javap` to disassemble.

Compared to most other compiled languages, Java generates one class file per class. the `objdump` method will simply read all all `*.class` inside the directory and disassembles them.

Also see: #12 #1364 #1058


Patch:
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 754389402..fc4c655d6 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -76,3 +76,5 @@ From oldest to newest contributor, we would like to thank:
 - [Sam Clegg](https://github.com/sbc100)
 - [Austin Morton](https://github.com/apmorton)
 - [Matt Hammerly](https://github.com/mhammerly)
+- [Christian Vonrüti](https://github.com/alshain)
+
diff --git a/etc/config/java.defaults.properties b/etc/config/java.defaults.properties
new file mode 100644
index 000000000..43773e716
--- /dev/null
+++ b/etc/config/java.defaults.properties
@@ -0,0 +1,17 @@
+# Default settings for Java
+compilers=&java
+compilerType=java
+versionFlag=-version
+objdumper=javap
+
+group.java.compilers=javacdefault
+compiler.javacdefault.exe=/usr/bin/javac
+compiler.javacdefault.name=javac default
+
+defaultCompiler=javacdefault
+demangler=
+postProcess=
+options=
+supportsBinary=false
+needsMulti=false
+supportsExecute=false
diff --git a/examples/java/default.java b/examples/java/default.java
new file mode 100644
index 000000000..ab3b236ef
--- /dev/null
+++ b/examples/java/default.java
@@ -0,0 +1,6 @@
+// Type your code here, or load an example.
+class Square {
+    static int square(int num) {
+        return num * num;
+    }
+}
\ No newline at end of file
diff --git a/lib/compilers/argument-parsers.js b/lib/compilers/argument-parsers.js
index ef43071de..19717dd2d 100644
--- a/lib/compilers/argument-parsers.js
+++ b/lib/compilers/argument-parsers.js
@@ -166,6 +166,13 @@ class ISPCParser extends BaseParser {
     }
 }
 
+class JavaParser extends BaseParser {
+
+    static parse(compiler) {
+        return JavaParser.getOptions(compiler, "-help").then(() => compiler);
+    }
+}
+
 class VCParser extends BaseParser {
     static parse(compiler) {
         return Promise.all([
@@ -248,6 +255,7 @@ module.exports = {
     Base: BaseParser,
     Clang: ClangParser,
     GCC: GCCParser,
+    Java: JavaParser,
     VC: VCParser,
     Pascal: PascalParser,
     ISPC: ISPCParser
diff --git a/lib/compilers/java.js b/lib/compilers/java.js
new file mode 100644
index 000000000..de5a514d4
--- /dev/null
+++ b/lib/compilers/java.js
@@ -0,0 +1,280 @@
+// Copyright (c) 2019, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+const BaseCompiler = require('../base-compiler'),
+    argumentParsers = require("./argument-parsers"),
+    fs = require('fs-extra'),
+    utils = require('../utils.js'),
+    path = require('path'),
+    logger = require('../logger').logger;
+
+class JavaCompiler extends BaseCompiler {
+    constructor(compilerInfo, env) {
+        // Default is to disable all "cosmetic" filters
+        if (!compilerInfo.disabledFilters) {
+            compilerInfo.disabledFilters = ['labels', 'directives', 'commentOnly', 'trim'];
+        }
+
+        super(compilerInfo, env);
+    }
+
+
+    objdump(outputFilename, result, maxSize) {
+        const dirPath = path.dirname(outputFilename);
+        return fs.readdir(dirPath)
+            .then(files => {
+                logger.verbose("Class files: ", files);
+                return files.filter(f => f.endsWith('.class'));
+            })
+            .then(classFiles => {
+                return Promise.all(classFiles.map(classFile => {
+                    const args = [
+                        // Prints out disassembled code, i.e., the instructions that comprise the Java bytecodes,
+                        // for each of the methods in the class.
+                        '-c',
+                        // Prints out line and local variable tables.
+                        '-l',
+                        classFile
+                    ];
+                    return this.exec(this.compiler.objdumper, args, {maxOutput: maxSize, customCwd: dirPath})
+                        .then(objResult => {
+                            const oneResult = {};
+                            oneResult.asm = objResult.stdout;
+                            if (objResult.code !== 0) {
+                                oneResult.asm = "<No output: javap returned " + objResult.code + ">";
+                            }
+                            return oneResult;
+                        });
+                }));
+            })
+            .then(results => {
+                const merged = {asm: []};
+                for (const result of results) {
+                    const asmBackup = merged.asm;
+                    Object.assign(merged, result);
+                    merged.asm = asmBackup;
+                    merged.asm.push(result.asm);
+                }
+
+                result.asm = merged.asm;
+                return result;
+            });
+    }
+
+
+    optionsForFilter(filters) {
+        // Forcibly enable javap
+        filters.binary = true;
+
+        return [
+            '-Xlint:all',
+        ];
+    }
+
+
+    getArgumentParser() {
+        return argumentParsers.Java;
+    }
+
+    getOutputFilename(dirPath) {
+        return path.join(dirPath, `${path.basename(this.compileFilename, this.lang.extensions[0])}.class`);
+    }
+
+
+    filterUserOptions(userOptions) {
+        const filteredOptions = [];
+        let toSkip = 0;
+
+        const oneArgBlacklist = [
+            // -d directory
+            // Sets the destination directory for class files.
+            "-d",
+            // -s directory
+            // Specifies the directory used to place the generated source files.
+            "-s",
+            // --source-path path or -sourcepath path
+            // Specifies where to find input source files.
+            "--source-path", "-sourcepath"];
+        for (const userOption of userOptions) {
+            if (toSkip > 0) {
+                toSkip--;
+                continue;
+            }
+            if (oneArgBlacklist.indexOf(userOption) !== -1) {
+                toSkip = 1;
+                continue;
+            }
+
+            filteredOptions.push(userOption);
+        }
+
+        return filteredOptions;
+    }
+
+    processAsm(result) {
+        // Handle "error" documents.
+        if (result.asm.indexOf("\n") === -1 && result.asm[0] === '<') {
+            return [{text: result.asm, source: null}];
+        }
+
+        // result.asm is an array of javap stdouts
+        const parseds = result.asm.map(asm => this.parseAsmForClass(asm));
+        // Sort class file outputs according to first source line they reference
+        parseds.sort((o1, o2) => o1.firstSourceLine - o2.firstSourceLine);
+
+        const segments = [];
+        parseds.forEach((parsed, classNumber) => {
+            if (classNumber > 0) {
+                // Separate classes with two line breaks
+                segments.push({text: "", source: null});
+                segments.push({text: "", source: null});
+            }
+            for (let i = 0; i < parsed.textsBeforeMethod.length; i++) {
+                // Line-based highlighting doesn't work if some segments span multiple lines,
+                // even if they don't have a source line number
+                // -> split the lines and create segment for each separately
+                for (const line of utils.splitLines(parsed.textsBeforeMethod[i])) {
+                    // javap output always starts with "Compiled from" on first line, discard these lines.
+                    if (line.startsWith("Compiled from")) {
+                        continue;
+                    }
+                    segments.push({
+                        text: line,
+                        source: null
+                    });
+                }
+
+                // textsBeforeMethod[last] is actually *after* the last method.
+                // Check whether there is a method following the text block
+                if (i < parsed.methods.length) {
+                    parsed.methods[i].instructions.forEach(({text, sourceLine}) => {
+                        segments.push({text: text, source: {file: null, line: sourceLine}});
+                    });
+                }
+            }
+        });
+        return segments;
+    }
+
+    parseAsmForClass(javapOut) {
+        const textsBeforeMethod = [];
+        const methods = [];
+        // javap output puts `    Code:` after every signature. (Line will not be shown to user)
+        // We use this to find the individual methods.
+        // Before the first `Code:` occurrence, there is the method signature as well as the name of the class.
+        // Subsequent matches are always followed by lists of assembly instructions as well as line info mappings
+
+        // Regex idea: make sure `Code:` is the only thing on the line. Also consume trailing line ending!
+        const [classNameAndFirstMethodSignature, ...codeAndLineNumberTables] = javapOut.split(/^\s+Code:\s*$\r?\n/m);
+        textsBeforeMethod.push(classNameAndFirstMethodSignature.trimEnd()); // possible trailing \r on windows
+
+        for (const codeAndLineNumberTable of codeAndLineNumberTables) {
+            const method = {
+                instructions: []
+            };
+            methods.push(method);
+
+
+            for (const codeLineCandidate of utils.splitLines(codeAndLineNumberTable)) {
+                // Match
+                //       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
+                const match = codeLineCandidate.match(/\s+(\d+): (.*)/);
+                if (match) {
+                    const instrOffset = Number.parseInt(match[1]);
+                    method.instructions.push({
+                        instrOffset: instrOffset,
+                        // Should an instruction ever not be followed by a line number table,
+                        // it might contain a trailing \r on Windows -> trim it, otherwise this would not be necessary
+                        text: codeLineCandidate.trimRight()
+                    });
+                } else {
+                    break;
+                }
+            }
+
+
+            let lineRegex = /line\s*(\d+):\s*(\d+)/g;
+            let m;
+            let currentInstr = 0;
+            let currentSourceLine = -1;
+            let lastIndex = -1;
+            do {
+                m = lineRegex.exec(codeAndLineNumberTable);
+                if (m) {
+                    // If exec doesn't find a match anymore, lineRegex.lastIndex will be reset to 0
+                    // therefore, cache value here on match
+                    lastIndex = lineRegex.lastIndex;
+                    const [, sourceLineS, instructionS] = m;
+                    logger.verbose("Found source mapping: ", sourceLineS, "to instruction", instructionS);
+                    const instrOffset = Number.parseInt(instructionS);
+
+                    // Some instructions don't receive an explicit line number.
+                    // They are all assigned to the previous explicit line number,
+                    // because the line consists of multiple instructions.
+                    while (currentInstr < method.instructions.length &&
+                        method.instructions[currentInstr].instrOffset !== instrOffset ) {
+
+                        if (currentSourceLine !== -1) {
+                            // instructions without explicit line number get assigned the last explicit/same line number
+                            method.instructions[currentInstr].sourceLine = currentSourceLine;
+                        } else {
+                            logger.error("Skipping over instruction even though currentSourceLine == -1");
+                        }
+                        currentInstr++;
+                    }
+
+                    const sourceLine = Number.parseInt(sourceLineS);
+                    currentSourceLine = sourceLine;
+                    method.instructions[currentInstr].sourceLine = currentSourceLine;
+
+                    if (typeof method.startLine === "undefined") {
+                        method.startLine = sourceLine;
+                    }
+                    // method.instructions.push({sourceLine: instrOffset});
+                }
+            } while (m);
+            if (lastIndex !== -1) {
+                // Get "interesting" text after the LineNumbers table (header of next method/tail of file)
+                // trimRight() because of trailing \r on Windows
+                textsBeforeMethod.push(codeAndLineNumberTable.substr(lastIndex).trimRight());
+            }
+
+            if (currentSourceLine !== -1) {
+                // Assign remaining instructions to the same source line
+                while (currentInstr + 1 < method.instructions.length){
+                    currentInstr++;
+                    method.instructions[currentInstr].sourceLine = currentSourceLine;
+                }
+            }
+        }
+        return {
+            // Used for sorting
+            firstSourceLine: methods.reduce((p, m) => p === -1 ? m.startLine : Math.min(p, m.startLine), -1),
+            methods: methods,
+            textsBeforeMethod
+        };
+    }
+}
+
+module.exports = JavaCompiler;
diff --git a/lib/languages.js b/lib/languages.js
index d4d0193d4..58b85b817 100644
--- a/lib/languages.js
+++ b/lib/languages.js
@@ -101,6 +101,12 @@ const languages = {
         extensions: ['.hs', '.haskell'],
         alias: []
     },
+    java: {
+        name: 'Java',
+        monaco: 'java',
+        extensions: ['.java'],
+        alias: []
+    },
     ocaml: {
         name: 'OCaml',
         monaco: 'ocaml',


Test patch:
diff --git a/test/java-tests.js b/test/java-tests.js
new file mode 100644
index 000000000..84c2d4478
--- /dev/null
+++ b/test/java-tests.js
@@ -0,0 +1,180 @@
+// Copyright (c) 2017, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+const chai = require('chai');
+const chaiAsPromised = require('chai-as-promised');
+const JavaCompiler = require('../lib/compilers/java');
+const CompilationEnvironment = require('../lib/compilation-env');
+const fs = require('fs-extra');
+const utils = require('../lib/utils');
+const properties = require('../lib/properties');
+
+chai.use(chaiAsPromised);
+chai.should();
+
+const languages = {
+    java: {id: 'java'}
+};
+
+const info = {
+    exe: null,
+    remote: true,
+    lang: languages.java.id
+};
+const compilerProps = new properties.CompilerProps(languages, properties.fakeProps({}));
+const ce = new CompilationEnvironment(compilerProps);
+
+function testJava(baseFolder, ...classNames) {
+    const compiler = new JavaCompiler(info, ce);
+
+    const asm = classNames.map(className => fs.readFileSync(`${baseFolder}/${className}.asm`).toString());
+
+    const output = utils.splitLines(fs.readFileSync(`${baseFolder}/output.asm`).toString());
+    const expectedSegments = output.map(line => {
+        const match = line.match(/^line (\d+):(.*)$/);
+        if (match) {
+            return {
+                text: match[2],
+                source: {
+                    line: parseInt(match[1]),
+                    file: null,
+                }
+            };
+        }
+        return {
+            text: line,
+            source: null,
+        };
+    });
+
+    const result = {
+        asm
+    };
+
+    const processed = compiler.processAsm(result);
+    processed.should.deep.equal(expectedSegments)
+}
+
+
+
+describe("Basic compiler setup", function () {
+    it("Should not crash on instantiation", function() {
+        new JavaCompiler(info, ce)
+    });
+
+
+    it("should ignore second param for getOutputFilename", function () {
+        // Because javac produces a class files based on user provided class names,
+        // it's not possible to determine the main class file before compilation/parsing
+        const compiler = new JavaCompiler(info, ce);
+        if (process.platform === 'win32') {
+            compiler.getOutputFilename('/tmp/', 'Ignored.java').should.equal('\\tmp\\example.class');
+        } else {
+            compiler.getOutputFilename('/tmp/', 'Ignored.java').should.equal('/tmp/example.class');
+        }
+    });
+
+    describe("Forbidden compiler arguments", function() {
+        it("JavaCompiler should not allow -d parameter", () => {
+            const compiler = new JavaCompiler(info, ce);
+            compiler.filterUserOptions(['hello', '-d', '--something', '--something-else']).should.deep.equal(
+                ['hello', '--something-else']
+            );
+            compiler.filterUserOptions(['hello', '-d']).should.deep.equal(
+                ['hello']
+            );
+            compiler.filterUserOptions(['-d', 'something', 'something-else']).should.deep.equal(
+                ['something-else']
+            );
+        });
+
+        it("JavaCompiler should not allow -s parameter", () => {
+            const compiler = new JavaCompiler(info, ce);
+            compiler.filterUserOptions(['hello', '-s', '--something', '--something-else']).should.deep.equal(
+                ['hello', '--something-else']
+            );
+            compiler.filterUserOptions(['hello', '-s']).should.deep.equal(
+                ['hello']
+            );
+            compiler.filterUserOptions(['-s', 'something', 'something-else']).should.deep.equal(
+                ['something-else']
+            );
+        });
+
+        it("JavaCompiler should not allow --source-path parameter", () => {
+            const compiler = new JavaCompiler(info, ce);
+            compiler.filterUserOptions(['hello', '--source-path', '--something', '--something-else']).should.deep.equal(
+                ['hello', '--something-else']
+            );
+            compiler.filterUserOptions(['hello', '--source-path']).should.deep.equal(
+                ['hello']
+            );
+            compiler.filterUserOptions(['--source-path', 'something', 'something-else']).should.deep.equal(
+                ['something-else']
+            );
+        });
+
+        it("JavaCompiler should not allow -sourcepath parameter", () => {
+            const compiler = new JavaCompiler(info, ce);
+            compiler.filterUserOptions(['hello', '-sourcepath', '--something', '--something-else']).should.deep.equal(
+                ['hello', '--something-else']
+            );
+            compiler.filterUserOptions(['hello', '-sourcepath']).should.deep.equal(
+                ['hello']
+            );
+            compiler.filterUserOptions(['-sourcepath', 'something', 'something-else']).should.deep.equal(
+                ['something-else']
+            );
+        });
+    });
+});
+
+describe("javap parsing", () => {
+    const compiler = new JavaCompiler(info, ce);
+
+    it("should handle errors", () => {
+        const result = {
+            asm: '<Compilation failed>',
+        };
+
+        compiler.processAsm(result).should.deep.equal([
+            {text: '<Compilation failed>', source: null}
+        ]);
+    });
+
+    it("Parses simple class with one method", () => {
+        return Promise.all([
+            testJava('test/java/square', 'javap-square'),
+        ]);
+    });
+
+    it("Preserves ordering of multiple classes", () => {
+        return Promise.all([
+            testJava('test/java/two-classes', 'ZFirstClass', 'ASecondClass'),
+            testJava('test/java/two-classes', 'ASecondClass', 'ZFirstClass')
+        ]);
+    });
+});
+
+
diff --git a/test/java/square/input.java b/test/java/square/input.java
new file mode 100644
index 000000000..ab3b236ef
--- /dev/null
+++ b/test/java/square/input.java
@@ -0,0 +1,6 @@
+// Type your code here, or load an example.
+class Square {
+    static int square(int num) {
+        return num * num;
+    }
+}
\ No newline at end of file
diff --git a/test/java/square/javap-square.asm b/test/java/square/javap-square.asm
new file mode 100644
index 000000000..ec04d008e
--- /dev/null
+++ b/test/java/square/javap-square.asm
@@ -0,0 +1,19 @@
+Compiled from "example.java"
+class Square {
+  Square();
+    Code:
+       0: aload_0
+       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
+       4: return
+    LineNumberTable:
+      line 2: 0
+
+  static int square(int);
+    Code:
+       0: iload_0
+       1: iload_0
+       2: imul
+       3: ireturn
+    LineNumberTable:
+      line 4: 0
+}
diff --git a/test/java/square/output.asm b/test/java/square/output.asm
new file mode 100644
index 000000000..80f6e5f25
--- /dev/null
+++ b/test/java/square/output.asm
@@ -0,0 +1,14 @@
+class Square {
+  Square();
+line 2:       0: aload_0
+line 2:       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
+line 2:       4: return
+
+
+  static int square(int);
+line 4:       0: iload_0
+line 4:       1: iload_0
+line 4:       2: imul
+line 4:       3: ireturn
+
+}
\ No newline at end of file
diff --git a/test/java/two-classes/ASecondClass.asm b/test/java/two-classes/ASecondClass.asm
new file mode 100644
index 000000000..84d1ae9b2
--- /dev/null
+++ b/test/java/two-classes/ASecondClass.asm
@@ -0,0 +1,19 @@
+Compiled from "example.java"
+class ASecondClass {
+  ASecondClass();
+    Code:
+       0: aload_0
+       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
+       4: return
+    LineNumberTable:
+      line 7: 0
+
+  static int square(int);
+    Code:
+       0: iload_0
+       1: iload_0
+       2: imul
+       3: ireturn
+    LineNumberTable:
+      line 9: 0
+}
diff --git a/test/java/two-classes/ZFirstClass.asm b/test/java/two-classes/ZFirstClass.asm
new file mode 100644
index 000000000..162f011f2
--- /dev/null
+++ b/test/java/two-classes/ZFirstClass.asm
@@ -0,0 +1,19 @@
+Compiled from "example.java"
+class ZFirstClass {
+  ZFirstClass();
+    Code:
+       0: aload_0
+       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
+       4: return
+    LineNumberTable:
+      line 1: 0
+
+  static int square(int);
+    Code:
+       0: iload_0
+       1: iload_0
+       2: imul
+       3: ireturn
+    LineNumberTable:
+      line 3: 0
+}
diff --git a/test/java/two-classes/input.java b/test/java/two-classes/input.java
new file mode 100644
index 000000000..0eab201c5
--- /dev/null
+++ b/test/java/two-classes/input.java
@@ -0,0 +1,11 @@
+class ZFirstClass {
+    static int square(int num) {
+        return num * num;
+    }
+}
+
+class ASecondClass {
+    static int square(int num) {
+        return num * num;
+    }
+}
\ No newline at end of file
diff --git a/test/java/two-classes/output.asm b/test/java/two-classes/output.asm
new file mode 100644
index 000000000..d317a9cc8
--- /dev/null
+++ b/test/java/two-classes/output.asm
@@ -0,0 +1,30 @@
+class ZFirstClass {
+  ZFirstClass();
+line 1:       0: aload_0
+line 1:       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
+line 1:       4: return
+
+
+  static int square(int);
+line 3:       0: iload_0
+line 3:       1: iload_0
+line 3:       2: imul
+line 3:       3: ireturn
+
+}
+
+
+class ASecondClass {
+  ASecondClass();
+line 7:       0: aload_0
+line 7:       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
+line 7:       4: return
+
+
+  static int square(int);
+line 9:       0: iload_0
+line 9:       1: iload_0
+line 9:       2: imul
+line 9:       3: ireturn
+
+}
\ No newline at end of file