diff options
author | Jeremy Rifkin <51220084+jeremy-rifkin@users.noreply.github.com> | 2023-01-29 13:22:30 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-29 13:22:30 -0500 |
commit | 6a6efaefe650c06fd93e0d52e0ae6b6e3b4d4718 (patch) | |
tree | ffeb8626d237499e91babfd329cb752497b19872 /lib/compilers/java.ts | |
parent | 87203f71cb1294703dcdbcb4abc3385b21083291 (diff) | |
download | compiler-explorer-6a6efaefe650c06fd93e0d52e0ae6b6e3b4d4718.tar.gz compiler-explorer-6a6efaefe650c06fd93e0d52e0ae6b6e3b4d4718.zip |
Tsify lib/compilers (#4609)gh-6026
Diffstat (limited to 'lib/compilers/java.ts')
-rw-r--r-- | lib/compilers/java.ts | 394 |
1 files changed, 394 insertions, 0 deletions
diff --git a/lib/compilers/java.ts b/lib/compilers/java.ts new file mode 100644 index 000000000..46820fad0 --- /dev/null +++ b/lib/compilers/java.ts @@ -0,0 +1,394 @@ +// 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. + +import path from 'path'; + +import fs from 'fs-extra'; + +import {ParsedAsmResult, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces'; +import {CompilerInfo} from '../../types/compiler.interfaces'; +import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces'; +import {unwrap} from '../assert'; +import {BaseCompiler} from '../base-compiler'; +import {logger} from '../logger'; +import * as utils from '../utils'; + +import {JavaParser} from './argument-parsers'; + +export class JavaCompiler extends BaseCompiler { + static get key() { + return 'java'; + } + + javaRuntime: string; + mainRegex: RegExp; + + constructor(compilerInfo: CompilerInfo, env) { + // Default is to disable all "cosmetic" filters + if (!compilerInfo.disabledFilters) { + compilerInfo.disabledFilters = ['labels', 'directives', 'commentOnly', 'trim']; + } + super(compilerInfo, env); + this.javaRuntime = this.compilerProps<string>(`compiler.${this.compiler.id}.runtime`); + this.mainRegex = /public static ?(.*?) void main\(java\.lang\.String\[]\)/; + } + + override getSharedLibraryPathsAsArguments() { + return []; + } + + override async objdump(outputFilename, result: any, maxSize: number) { + const dirPath = path.dirname(outputFilename); + const files = await fs.readdir(dirPath); + logger.verbose('Class files: ', files); + const results = await Promise.all( + files + .filter(f => f.endsWith('.class')) + .map(async 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', + // Private things + '-p', + // Final constants + '-constants', + // Verbose - ideally we'd enable this and then we get constant pools too. Needs work to parse. + //'-v', + classFile, + ]; + const objResult = await this.exec(this.compiler.objdumper, args, { + maxOutput: maxSize, + customCwd: dirPath, + }); + const oneResult: ParsedAsmResult = { + asm: [ + { + text: objResult.stdout, + }, + ], + }; + + if (objResult.code === 0) { + oneResult.objdumpTime = objResult.execTime; + } else { + oneResult.asm = [ + { + text: `<No output: javap returned ${objResult.code}>`, + }, + ]; + } + return oneResult; + }), + ); + + const merged = {asm: [] as ParsedAsmResultLine[][]}; + 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; + } + + override optionsForFilter(filters: ParseFiltersAndOutputOptions) { + // Forcibly enable javap + filters.binary = true; + + return ['-Xlint:all', '-encoding', 'utf8']; + } + + override async handleInterpreting(key, executeParameters) { + const compileResult = await this.getOrBuildExecutable(key); + if (compileResult.code === 0) { + executeParameters.args = [ + '-Xss136K', // Reduce thread stack size + '-XX:CICompilerCount=2', // Reduce JIT compilation threads. 2 is minimum + '-XX:-UseDynamicNumberOfCompilerThreads', + '-XX:-UseDynamicNumberOfGCThreads', + '-XX:+UseSerialGC', // Disable parallell/concurrent garbage collector + await this.getMainClassName(compileResult.dirPath), + '-cp', + compileResult.dirPath, + ...executeParameters.args, + ]; + const result = await this.runExecutable(this.javaRuntime, executeParameters, compileResult.dirPath); + return { + ...result, + didExecute: true, + buildResult: compileResult, + }; + } else { + return { + stdout: compileResult.stdout, + stderr: compileResult.stderr, + code: compileResult.code, + didExecute: false, + buildResult: compileResult, + timedOut: false, + }; + } + } + + async getMainClassName(dirPath: string) { + const maxSize = this.env.ceProps('max-asm-size', 64 * 1024 * 1024); + const files = await fs.readdir(dirPath); + const results = await Promise.all( + files + .filter(f => f.endsWith('.class')) + .map(async classFile => { + const options = { + maxOutput: maxSize, + customCwd: dirPath, + }; + const objResult = await this.exec(this.compiler.objdumper, [classFile], options); + if (objResult.code !== 0) { + return null; + } + + if (this.mainRegex.test(objResult.stdout)) { + return classFile; + } + return null; + }), + ); + + const candidates = results.filter(file => file !== null); + if (candidates.length > 0) { + // In case of multiple candidates, we'll just take the first one. + const fileName = unwrap(candidates[0]); + return fileName.substring(0, fileName.lastIndexOf('.')); + } + // We were unable to find a main method, let's error out assuming "Main" + return 'Main'; + } + + override getArgumentParser() { + return JavaParser; + } + + override getOutputFilename(dirPath: string) { + return path.join(dirPath, `${path.basename(this.compileFilename, this.lang.extensions[0])}.class`); + } + + filterUserOptionsWithArg(userOptions: string[], oneArgForbiddenList: Set<string>) { + const filteredOptions: string[] = []; + let toSkip = 0; + + for (const userOption of userOptions) { + if (toSkip > 0) { + toSkip--; + continue; + } + if (oneArgForbiddenList.has(userOption)) { + toSkip = 1; + continue; + } + + filteredOptions.push(userOption); + } + + return filteredOptions; + } + + override filterUserOptions(userOptions: string[]) { + const oneArgForbiddenList = new Set([ + // -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', + ]); + + return this.filterUserOptionsWithArg(userOptions, oneArgForbiddenList); + } + + override processAsm(result) { + // Handle "error" documents. + if (!result.asm.includes('\n') && 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: ParsedAsmResultLine[] = []; + for (const [classNumber, parsed] of parseds.entries()) { + if (classNumber > 0) { + // Separate classes with two line breaks + segments.push({text: '', source: null}, {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) { + for (const {text, sourceLine} of parsed.methods[i].instructions) { + segments.push({text: text, source: {file: null, line: sourceLine}}); + } + } + } + } + return {asm: segments}; + } + + parseAsmForClass(javapOut) { + const textsBeforeMethod: string[] = []; + const methods: {instructions: any[]; startLine?: number}[] = []; + // 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: [], + } as typeof methods[0]; + methods.push(method); + + for (const codeLineCandidate of utils.splitLines(codeAndLineNumberTable)) { + // Match + // 1: invokespecial #1 // Method java/lang/Object."<init>":()V + // Or match the "default: <code>" block inside a lookupswitch instruction + const match = codeLineCandidate.match(/\s+(\d+|default): (.*)/); + 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.trimEnd(), + }); + } else { + // Attempt to match the closing } of a lookupswitch. If we don't include the closing bracket, then + // the brackets will be misaligned, and it may be confusing to read. + const isClosingCurlyBrace = codeLineCandidate.match(/\s+}/); + if (isClosingCurlyBrace) { + // Put closing curly brace in asm output + method.instructions.push({ + text: codeLineCandidate.trimEnd(), + }); + continue; + } + break; + } + } + + const 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) { + logger.error('Skipping over instruction even though currentSourceLine == -1'); + } else { + // instructions without explicit line number get assigned the last explicit/same line number + method.instructions[currentInstr].sourceLine = currentSourceLine; + } + currentInstr++; + } + + const sourceLine = Number.parseInt(sourceLineS); + currentSourceLine = sourceLine; + if (method.instructions[currentInstr]) { + method.instructions[currentInstr].sourceLine = currentSourceLine; + } + + if (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).trimEnd()); + } + + 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 ? unwrap(m.startLine) : Math.min(p, unwrap(m.startLine))), + -1, + ), + methods: methods, + textsBeforeMethod, + }; + } +} |