diff options
Diffstat (limited to 'lib/compilers')
-rw-r--r-- | lib/compilers/_all.js | 1 | ||||
-rw-r--r-- | lib/compilers/golang.ts (renamed from lib/compilers/golang.js) | 136 | ||||
-rw-r--r-- | lib/compilers/hlsl.ts (renamed from lib/compilers/hlsl.js) | 25 | ||||
-rw-r--r-- | lib/compilers/rga.ts | 276 | ||||
-rw-r--r-- | lib/compilers/rust.ts | 32 | ||||
-rw-r--r-- | lib/compilers/wine-vc.js | 5 | ||||
-rw-r--r-- | lib/compilers/zig.js | 14 |
7 files changed, 401 insertions, 88 deletions
diff --git a/lib/compilers/_all.js b/lib/compilers/_all.js index 9abb0e5ef..3224acc03 100644 --- a/lib/compilers/_all.js +++ b/lib/compilers/_all.js @@ -75,6 +75,7 @@ export {PonyCompiler} from './pony'; export {PPCICompiler} from './ppci'; export {PtxAssembler} from './ptxas'; export {PythonCompiler} from './python'; +export {RGACompiler} from './rga'; export {RubyCompiler} from './ruby'; export {RustcCgGCCCompiler} from './rustc-cg-gcc'; export {RustCompiler} from './rust'; diff --git a/lib/compilers/golang.js b/lib/compilers/golang.ts index 8aa0fd985..b0abb1498 100644 --- a/lib/compilers/golang.js +++ b/lib/compilers/golang.ts @@ -1,4 +1,4 @@ -// Copyright (c) 2016, Compiler Explorer Authors +// Copyright (c) 2022, Compiler Explorer Authors // All rights reserved. // // Redistribution and use in source and binary forms, with or without @@ -24,6 +24,7 @@ import _ from 'underscore'; +import {ResultLine} from '../../types/resultline/resultline.interfaces'; import {BaseCompiler} from '../base-compiler'; import * as utils from '../utils'; @@ -34,40 +35,61 @@ import {ClangParser} from './argument-parsers'; // x86 -> j, b // arm -> cb, tb // s390x -> cmpb, cmpub -const jumpRe = /^(j|b|cb|tb|cmpb|cmpub).*/i; +const JUMP_RE = /^(j|b|cb|tb|cmpb|cmpub).*/i; +const LINE_RE = /^\s+(0[Xx]?[\dA-Za-z]+)?\s?(\d+)\s*\(([^:]+):(\d+)\)\s*([A-Z]+)(.*)/; +const UNKNOWN_RE = /^\s+(0[Xx]?[\dA-Za-z]+)?\s?(\d+)\s*\(<unknown line number>\)\s*([A-Z]+)(.*)/; +const FUNC_RE = /TEXT\s+[".]*(\S+)\(SB\)/; +const LOGGING_RE = /^[^:]+:\d+:(\d+:)?\s.*/; +const DECIMAL_RE = /(\s+)(\d+)(\s?)$/; + +type GoEnv = { + GOROOT?: string; + GOARCH?: string; + GOOS?: string; +}; export class GolangCompiler extends BaseCompiler { + private readonly GOENV: GoEnv; + static get key() { return 'golang'; } constructor(compilerInfo, env) { super(compilerInfo, env); - this.goroot = this.compilerProps(`compiler.${this.compiler.id}.goroot`); - this.goarch = this.compilerProps(`compiler.${this.compiler.id}.goarch`); - this.goos = this.compilerProps(`compiler.${this.compiler.id}.goos`); + const goroot = this.compilerProps(`compiler.${this.compiler.id}.goroot`); + const goarch = this.compilerProps(`compiler.${this.compiler.id}.goarch`); + const goos = this.compilerProps(`compiler.${this.compiler.id}.goos`); + + this.GOENV = {}; + if (goroot) { + this.GOENV.GOROOT = goroot; + } + if (goarch) { + this.GOENV.GOARCH = goarch; + } + if (goos) { + this.GOENV.GOOS = goos; + } } - convertNewGoL(code) { - const re = /^\s+(0[Xx]?[\dA-Za-z]+)?\s?(\d+)\s*\(([^:]+):(\d+)\)\s*([A-Z]+)(.*)/; - const reUnknown = /^\s+(0[Xx]?[\dA-Za-z]+)?\s?(\d+)\s*\(<unknown line number>\)\s*([A-Z]+)(.*)/; - const reFunc = /TEXT\s+[".]*(\S+)\(SB\)/; - let prevLine = null; - let file = null; + convertNewGoL(code: ResultLine[]): string { + let prevLine: string | null = null; + let file: string | null = null; let fileCount = 0; - let func = null; - const funcCollisions = {}; - const labels = {}; - const usedLabels = {}; + let func: string | null = null; + const funcCollisions: Record<string, number> = {}; + const labels: Record<string, boolean> = {}; + const usedLabels: Record<string, boolean> = {}; const lines = code.map(obj => { - let pcMatch = null; - let fileMatch = null; - let lineMatch = null; - let ins = null; - let args = null; + let pcMatch: string | null = null; + let fileMatch: string | null = null; + let lineMatch: string | null = null; + let ins: string | null = null; + let args: string | null = null; const line = obj.text; - let match = line.match(re); + let match = line.match(LINE_RE); if (match) { pcMatch = match[2]; fileMatch = match[3]; @@ -75,17 +97,17 @@ export class GolangCompiler extends BaseCompiler { ins = match[5]; args = match[6]; } else { - match = line.match(reUnknown); + match = line.match(UNKNOWN_RE); if (match) { pcMatch = match[2]; ins = match[3]; args = match[4]; } else { - return null; + return []; } } - match = line.match(reFunc); + match = line.match(FUNC_RE); if (match) { // Normalize function name. func = match[1].replace(/[()*.]+/g, '_'); @@ -103,12 +125,12 @@ export class GolangCompiler extends BaseCompiler { funcCollisions[func] = collisions; } - let res = []; + const res: string[] = []; if (pcMatch && !labels[pcMatch]) { // Create pseudo-label. let label = pcMatch.replace(/^0{0,4}/, ''); let suffix = ''; - if (funcCollisions[func] > 0) { + if (func && funcCollisions[func] > 0) { suffix = `_${funcCollisions[func]}`; } @@ -130,32 +152,37 @@ export class GolangCompiler extends BaseCompiler { prevLine = lineMatch; } - args = this.replaceJump(func, funcCollisions[func], ins, args, usedLabels); - res.push(`\t${ins}${args}`); + if (func) { + args = this.replaceJump(func, funcCollisions[func], ins, args, usedLabels); + res.push(`\t${ins}${args}`); + } return res; }); // Find unused pseudo-labels so they can be filtered out. const unusedLabels = _.mapObject(labels, (val, key) => !usedLabels[key]); - return _.chain(lines) - .flatten() - .compact() + return lines + .flat() .filter(line => !unusedLabels[line]) - .value() .join('\n'); } - replaceJump(func, collisions, ins, args, usedLabels) { + replaceJump( + func: string, + collisions: number, + ins: string, + args: string, + usedLabels: Record<string, boolean>, + ): string { // Check if last argument is a decimal number. - const re = /(\s+)(\d+)(\s?)$/; - const match = args.match(re); + const match = args.match(DECIMAL_RE); if (!match) { return args; } // Check instruction has a jump prefix - if (jumpRe.test(ins)) { + if (JUMP_RE.test(ins)) { let label = `${func}_pc${match[2]}`; if (collisions > 0) { label += `_${collisions}`; @@ -167,16 +194,16 @@ export class GolangCompiler extends BaseCompiler { return args; } - extractLogging(stdout) { - let filepath = `./${this.compileFilename}`; - const reLogging = /^[^:]+:\d+:(\d+:)?\s.*/; + extractLogging(stdout: ResultLine[]): string { + const filepath = `./${this.compileFilename}`; + return stdout - .filter(obj => obj.text.match(reLogging)) + .filter(obj => obj.text.match(LOGGING_RE)) .map(obj => obj.text.replace(filepath, '<source>')) .join('\n'); } - async postProcess(result) { + override async postProcess(result) { let out = result.stderr; if (this.compiler.id === '6g141') { out = result.stdout; @@ -185,14 +212,14 @@ export class GolangCompiler extends BaseCompiler { result.asm = this.convertNewGoL(out); result.stderr = null; result.stdout = utils.parseOutput(logging, result.inputFilename); - return [result, '']; + return Promise.all([result, '']); } - getSharedLibraryPathsAsArguments() { + override getSharedLibraryPathsAsArguments() { return []; } - optionsForFilter(filters, outputFilename, userOptions) { + override optionsForFilter(filters, outputFilename, userOptions) { // If we're dealing with an older version... if (this.compiler.id === '6g141') { return ['tool', '6g', '-g', '-o', outputFilename, '-S']; @@ -206,7 +233,7 @@ export class GolangCompiler extends BaseCompiler { } } - filterUserOptions(userOptions) { + override filterUserOptions(userOptions) { if (this.compiler.id === '6g141') { return userOptions; } @@ -214,21 +241,14 @@ export class GolangCompiler extends BaseCompiler { return []; } - getDefaultExecOptions() { - const execOptions = super.getDefaultExecOptions(); - if (this.goroot) { - execOptions.env.GOROOT = this.goroot; - } - if (this.goarch) { - execOptions.env.GOARCH = this.goarch; - } - if (this.goos) { - execOptions.env.GOOS = this.goos; - } - return execOptions; + override getDefaultExecOptions() { + return { + ...super.getDefaultExecOptions(), + ...this.GOENV, + }; } - getArgumentParser() { + override getArgumentParser() { return ClangParser; } } diff --git a/lib/compilers/hlsl.js b/lib/compilers/hlsl.ts index 25858839e..2925a0154 100644 --- a/lib/compilers/hlsl.js +++ b/lib/compilers/hlsl.ts @@ -24,6 +24,7 @@ import path from 'path'; +import {ParseFilters} from '../../types/features/filters.interfaces'; import {BaseCompiler} from '../base-compiler'; export class HLSLCompiler extends BaseCompiler { @@ -31,21 +32,39 @@ export class HLSLCompiler extends BaseCompiler { return 'hlsl'; } - constructor(info, env) { + constructor(info: any, env: any) { super(info, env); this.compiler.supportsIntel = false; } /* eslint-disable no-unused-vars */ - optionsForFilter(filters, outputFilename) { + override optionsForFilter(filters: ParseFilters, outputFilename: string, userOptions?: string[]): string[] { return [ + '-Zi', // Embed debug information to get DXIL line associations + '-Qembed_debug', // Silences the warning associated with embedded debug information `-Fc ${outputFilename}`, // Output object ]; } /* eslint-enable no-unused-vars */ - getIrOutputFilename(inputFilename) { + override filterUserOptions(userOptions: any) { + // RGA supports a non-standard flag --asic [ASIC] which must be removed when compiling with DXC + const options = userOptions.slice(0); + // Scan for the RGA-specific argument --asic and strip it and its corresponding argument + // Assumes the argument exists at most once (compilation will fail if supplied more than + // once regardless) + for (let i = 0; i !== options.length; ++i) { + const option = options[i]; + if (option === '--asic') { + options.splice(i, 2); + break; + } + } + return options; + } + + override getIrOutputFilename(inputFilename: string) { return this.getOutputFilename(path.dirname(inputFilename), this.outputFilebase).replace('.s', '.dxil'); } } diff --git a/lib/compilers/rga.ts b/lib/compilers/rga.ts new file mode 100644 index 000000000..aec9d2084 --- /dev/null +++ b/lib/compilers/rga.ts @@ -0,0 +1,276 @@ +// Copyright (c) 2022, 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 {readdir, readFile, rename, writeFile} from 'fs-extra'; + +import {CompilationResult, ExecutionOptions} from '../../types/compilation/compilation.interfaces'; +import {ParseFilters} from '../../types/features/filters.interfaces'; +import {BaseCompiler} from '../base-compiler'; +import * as exec from '../exec'; +import {logger} from '../logger'; + +interface ASICSelection { + asic?: string; + error?: string; + printASICs?: boolean; +} + +// RGA := Radeon GPU Analyzer +export class RGACompiler extends BaseCompiler { + private dxcPath: string; + + static get key() { + return 'rga'; + } + + constructor(info: any, env: any) { + super(info, env); + + this.compiler.supportsIntel = false; + this.dxcPath = this.compilerProps(`compiler.${this.compiler.id}.dxcPath`); + logger.debug(`RGA compiler ${this.compiler.id} configured to use DXC at ${this.dxcPath}`); + } + + override optionsForFilter(filters: ParseFilters, outputFilename: any, userOptions?: any): any[] { + return [outputFilename]; + } + + extractASIC(dxcArgs: string[]): ASICSelection { + // Scan dxc args for an `--asic` argument that should be stripped and passed later to RGA + // Default to RDNA2 + let asic = 'gfx1030'; + let printASICs = true; + for (let i = 0; i !== dxcArgs.length; ++i) { + const arg = dxcArgs[i]; + if (arg === '--asic') { + // NOTE: the last arguments are the input source file and -spirv, so check + // if --asic immediately precedes that + if (i === dxcArgs.length - 3) { + return { + error: '--asic flag supplied without subsequent ASIC!', + }; + } + asic = dxcArgs[i + 1]; + // Do a quick sanity check to determine if a valid ASIC was supplied + if (!asic.startsWith('gfx')) { + return { + error: `The argument immediately following --asic doesn't appear to be a valid ASIC. +Please supply an ASIC from the following options:`, + printASICs: true, + }; + } + // Remove these two arguments from the dxcArgs list + dxcArgs.splice(i, 2); + + // If the user supplied a specific ASIC, don't bother printing available ASIC options + printASICs = false; + break; + } + } + + return { + asic, + printASICs, + }; + } + + execTime(startTime: bigint, endTime: bigint): string { + return ((endTime - startTime) / BigInt(1000000)).toString(); + } + + override async runCompiler( + compiler: string, + options: string[], + inputFilename: string, + execOptions: ExecutionOptions, + ): Promise<CompilationResult> { + if (!execOptions) { + execOptions = this.getDefaultExecOptions(); + } + + if (!execOptions.customCwd) { + execOptions.customCwd = path.dirname(inputFilename); + } + + const result = await this.execDXCandRGA(compiler, options, execOptions); + result.inputFilename = inputFilename; + const transformedInput = result.filenameTransform(inputFilename); + this.parseCompilationOutput(result, transformedInput); + return result; + } + + async execDXCandRGA(filepath: string, args: string[], execOptions: ExecutionOptions): Promise<any> { + // RGA is invoked in two steps. First, DXC is invoked to compile the SPIR-V output of the HLSL file. + // Next, RGA is invoked to consume the SPIR-V output and produce the requested ISA. + + // Track the total time spent instead of relying on executeDirect's internal timing facility + const startTime = process.hrtime.bigint(); + + // The first argument is the target output file + const outputFile = args[0]; + const outputDir = path.dirname(outputFile); + const spvTemp = 'output.spv.txt'; + logger.debug(`Intermediate SPIR-V output: ${spvTemp}`); + + const dxcArgs = args.slice(1); + if (!dxcArgs.includes('-spirv')) { + dxcArgs.push('-spirv'); + } + logger.debug(`DXC args: ${dxcArgs}`); + + const asicSelection = this.extractASIC(dxcArgs); + if (asicSelection.error) { + // Invalid user ASIC selected, bail out immediately + const endTime = process.hrtime.bigint(); + + // Synthesize a faux-execution result (see promise resolution code in executeDirect) + return { + code: -1, + okToCache: true, + filenameTransform: x => x, + stdout: asicSelection.error, + execTime: this.execTime(startTime, endTime), + }; + } + + const dxcResult = await exec.execute(this.dxcPath, dxcArgs, execOptions); + if (dxcResult.code !== 0) { + // Failed to compile SPIR-V intermediate product. Exit immediately with DXC invocation result. + const endTime = process.hrtime.bigint(); + dxcResult.execTime = this.execTime(startTime, endTime); + return dxcResult; + } + + try { + await writeFile(path.join(outputDir, spvTemp), dxcResult.stdout); + } catch (e) { + const endTime = process.hrtime.bigint(); + return { + code: -1, + okToCache: true, + filenameTransform: x => x, + stdout: 'Failed to emit intermediate SPIR-V result.', + execTime: this.execTime(startTime, endTime), + }; + } + + let registerAnalysisFile = 'livereg.txt'; + const rgaArgs = [ + '-s', + 'vk-spv-txt-offline', + '-c', + asicSelection.asic, + '--isa', + outputFile, + '--livereg', + registerAnalysisFile, + spvTemp, + ]; + logger.debug(`RGA args: ${rgaArgs}`); + + const rgaResult = await exec.execute(filepath, rgaArgs, execOptions); + if (rgaResult.code !== 0) { + // Failed to compile AMD ISA + const endTime = process.hrtime.bigint(); + rgaResult.execTime = this.execTime(startTime, endTime); + return rgaResult; + } + + // RGA doesn't emit the exact file we requested. It prepends the requested GPU + // architecture and appends the shader type (with underscore separators). Here, + // we rename the generated file to the output file Compiler Explorer expects. + + const files = await readdir(outputDir, {encoding: 'utf-8'}); + for (const file of files) { + if (file.startsWith((asicSelection.asic as string) + '_output')) { + await rename(path.join(outputDir, file), outputFile); + + registerAnalysisFile = path.join(outputDir, file.replace('output', 'livereg').replace('.s', '.txt')); + // The register analysis file contains a legend, and register liveness data + // for each line of disassembly. Interleave those lines into the final output + // as assembly comments. + const asm = await readFile(outputFile, 'utf-8'); + const asmLines = asm.split(/\r?\n/); + const analysis = await readFile(registerAnalysisFile, 'utf-8'); + const analysisLines = analysis.split(/\r?\n/); + + // The first few lines of the register analysis are the legend. Emit those lines + // as comments at the start of the output. + let analysisOffset = analysisLines.indexOf(''); + analysisOffset += 3; + const epilogueOffset = analysisLines.indexOf('', analysisOffset); + const outputAsm = analysisLines.slice(epilogueOffset + 1).map(line => `; ${line}`); + outputAsm.push(...analysisLines.slice(0, analysisOffset).map(line => `; ${line}`), '\n'); + + let asmOffset = asmLines.indexOf(''); + outputAsm.push(...asmLines.slice(0, asmOffset)); + asmOffset += 1; + + // Perform the interleave + for (let i = 0; i !== asmOffset + asmLines.length; ++i) { + if (i + analysisOffset >= epilogueOffset) { + outputAsm.push(...asmLines.slice(i)); + break; + } + + // Check if this line of assembly corresponds to a label. If so, emit the asm line + // and continue from the next line (the register analysis file operates on instructions, + // and labels are not considered instructions) + if (asmLines[i + asmOffset].startsWith('label')) { + outputAsm.push(asmLines[i + asmOffset]); + ++asmOffset; + --i; + continue; + } + + outputAsm.push(`; ${analysisLines[i + analysisOffset]}`, asmLines[i + asmOffset]); + } + + await writeFile(outputFile, outputAsm.join('\n')); + + if (asicSelection.printASICs) { + rgaResult.stdout += `ISA compiled with the default AMD ASIC (Radeon RX 6800 series RDNA2). +To override this, pass --asic [ASIC] to the options above (nonstandard DXC option), +where [ASIC] corresponds to one of the following options:`; + + const asics = await exec.execute(filepath, ['-s', 'vk-spv-txt-offline', '-l'], execOptions); + rgaResult.stdout += '\n'; + rgaResult.stdout += asics.stdout; + } + + const endTime = process.hrtime.bigint(); + rgaResult.execTime = this.execTime(startTime, endTime); + return rgaResult; + } + } + + // Arriving here means the expected ISA result wasn't emitted. Synthesize an error. + const endTime = process.hrtime.bigint(); + rgaResult.execTime = this.execTime(startTime, endTime); + rgaResult.stdout += `\nRGA didn't emit expected ISA output.`; + return rgaResult; + } +} diff --git a/lib/compilers/rust.ts b/lib/compilers/rust.ts index 89b52533d..f1d0d3a89 100644 --- a/lib/compilers/rust.ts +++ b/lib/compilers/rust.ts @@ -35,6 +35,7 @@ import {RustParser} from './argument-parsers'; export class RustCompiler extends BaseCompiler { linker: string; + static get key() { return 'rust'; } @@ -69,24 +70,19 @@ export class RustCompiler extends BaseCompiler { override getIncludeArguments(libraries) { const includeFlag = '--extern'; - return _.flatten( - _.map(libraries, selectedLib => { - const foundVersion = this.findLibVersion(selectedLib); - if (!foundVersion) return false; - if (!foundVersion.name) return false; - const list: string[] = []; - const lowercaseLibName = foundVersion.name.replaceAll('-', '_'); - for (const rlib of foundVersion.path) { - list.push( - includeFlag, - `${lowercaseLibName}=${foundVersion.name}/build/debug/${rlib}`, - '-L', - `dependency=${foundVersion.name}/build/debug/deps`, - ); - } - return list; - }), - ); + return libraries.flatMap(selectedLib => { + const foundVersion = this.findLibVersion(selectedLib); + if (!foundVersion || !foundVersion.name) return []; + const lowercaseLibName = foundVersion.name.replaceAll('-', '_'); + return foundVersion.path.flatMap(rlib => { + return [ + includeFlag, + `${lowercaseLibName}=${foundVersion.name}/build/debug/${rlib}`, + '-L', + `dependency=${foundVersion.name}/build/debug/deps`, + ]; + }); + }); } override orderArguments( diff --git a/lib/compilers/wine-vc.js b/lib/compilers/wine-vc.js index 653f6289e..be60b94d7 100644 --- a/lib/compilers/wine-vc.js +++ b/lib/compilers/wine-vc.js @@ -51,7 +51,10 @@ export class WineVcCompiler extends BaseCompiler { execOptions = this.getDefaultExecOptions(); } - execOptions.customCwd = path.dirname(inputFilename).substr(2); + execOptions.customCwd = path.dirname(inputFilename); + if (inputFilename.startsWith('Z:')) { + execOptions.customCwd = execOptions.customCwd.substr(2); + } return super.runCompiler(compiler, options, inputFilename, execOptions); } diff --git a/lib/compilers/zig.js b/lib/compilers/zig.js index 1a9d0512d..d524fc1de 100644 --- a/lib/compilers/zig.js +++ b/lib/compilers/zig.js @@ -132,14 +132,12 @@ export class ZigCompiler extends BaseCompiler { } getIncludeArguments(libraries) { - return _.flatten( - _.map(libraries, selectedLib => { - const foundVersion = this.findLibVersion(selectedLib); - if (!foundVersion) return false; - // Zig should not have more than 1 path - return ['--pkg-begin', foundVersion.name, foundVersion.path, '--pkg-end']; - }), - ); + return libraries.flatMap(selectedLib => { + const foundVersion = this.findLibVersion(selectedLib); + if (!foundVersion) return []; + // Zig should not have more than 1 path, but it's still an array so spread it + return ['--pkg-begin', foundVersion.name, ...foundVersion.path, '--pkg-end']; + }); } getIrOutputFilename(inputFilename) { |