From bb1f491b58d7c9727a3bf4962fddba6b8755a6b7 Mon Sep 17 00:00:00 2001 From: Steve Date: Fri, 11 Feb 2022 22:22:08 +0800 Subject: [PATCH] Implement asm-parser for dotnet (#3334) --- README.md | 4 +- lib/asm-parser-dotnet.ts | 194 +++++++++++++++++++++++++++++++++++++++ lib/compilers/dotnet.ts | 33 +------ 3 files changed, 201 insertions(+), 30 deletions(-) create mode 100644 lib/asm-parser-dotnet.ts diff --git a/README.md b/README.md index 989b76524..360763e31 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ # Compiler Explorer -**Compiler Explorer** is an interactive compiler exploration website. Edit code in C, C++, Rust, Go, D, Haskell, Swift, Pascal, [ispc](https://ispc.github.io/), Python, Java - or in any of the other [31 supported languages](https://godbolt.org/api/languages), and see how that code looks after being compiled in real time. +**Compiler Explorer** is an interactive compiler exploration website. Edit code in C, C++, C#, F#, Rust, Go, D, Haskell, Swift, Pascal, [ispc](https://ispc.github.io/), Python, Java + or in any of the other [30+ supported languages](https://godbolt.org/api/languages), and see how that code looks after being compiled in real time. Multiple compilers are supported for each language, many different tools and visualisations are available, and the UI layout is configurable (thanks to [GoldenLayout](https://www.golden-layout.com/)). diff --git a/lib/asm-parser-dotnet.ts b/lib/asm-parser-dotnet.ts new file mode 100644 index 000000000..98ae2221e --- /dev/null +++ b/lib/asm-parser-dotnet.ts @@ -0,0 +1,194 @@ +// 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 * as utils from './utils'; + +type InlineLabel = { name: string, range: { startCol: number, endCol: number } }; +type Source = { file: string, line: number }; + +export class DotNetAsmParser { + scanLabelsAndMethods(asmLines: string[], removeUnused: boolean) { + const labelDef: Record = {}; + const methodDef: Record = {}; + const labelUsage: Record = {}; + const methodUsage: Record = {}; + const allAvailable: string[] = []; + const usedLabels: string[] = []; + + const methodRefRe = /^\w+\s+\[(.*)]/g; + const tailCallRe = /^tail\.jmp\s+\[.*?](.*)/g; + const labelRefRe = /^\w+\s+.*?(G_M\w+)/g; + + for (const line in asmLines) { + const trimmedLine = asmLines[line].trim(); + if (!trimmedLine || trimmedLine.startsWith(';')) continue; + if (trimmedLine.endsWith(':')) { + if (trimmedLine.includes('(')) { + methodDef[line] = trimmedLine.substring(0, trimmedLine.length - 1); + allAvailable.push(methodDef[line]); + } + else { + labelDef[line] = { + name: trimmedLine.substring(0, trimmedLine.length - 1), + remove: false, + }; + allAvailable.push(labelDef[line].name); + } + continue; + } + + const labelResult = trimmedLine.matchAll(labelRefRe).next(); + if (!labelResult.done) { + const name = labelResult.value[1]; + const index = asmLines[line].indexOf(name) + 1; + labelUsage[line] = { + name: labelResult.value[1], + range: { startCol: index, endCol: index + name.length }, + }; + usedLabels.push(labelResult.value[1]); + } + + let methodResult = trimmedLine.matchAll(methodRefRe).next(); + if (methodResult.done) methodResult = trimmedLine.matchAll(tailCallRe).next(); + if (!methodResult.done) { + const name = methodResult.value[1]; + const index = asmLines[line].indexOf(name) + 1; + methodUsage[line] = { + name: methodResult.value[1], + range: { startCol: index, endCol: index + name.length }, + }; + } + } + + if (removeUnused) { + for (const line in labelDef) { + if (!usedLabels.includes(labelDef[line].name)) { + labelDef[line].remove = true; + } + } + } + + return { + labelDef, + labelUsage, + methodDef, + methodUsage, + allAvailable, + }; + } + + cleanAsm(asmLines: string[]) { + const cleanedAsm = []; + + for (const line of asmLines) { + if (!line) continue; + if (line.startsWith('; Assembly listing for method')) { + if (cleanedAsm.length > 0) cleanedAsm.push(''); + // ; Assembly listing for method ConsoleApplication.Program:Main(System.String[]) + // ^ This character is the 31st character in this string. + // `substring` removes the first 30 characters from it and uses the rest as a label. + cleanedAsm.push(line.substring(30) + ':'); + continue; + } + + if (line.startsWith('Emitting R2R PE file')) continue; + if (line.startsWith(';') && !line.startsWith('; Emitting')) continue; + + cleanedAsm.push(line); + } + + return cleanedAsm; + } + + process(asmResult: string, filters) { + const startTime = process.hrtime.bigint(); + + const asm: { + text: string, + source: Source | null, + labels: InlineLabel[], + }[] = []; + let labelDefinitions: [string, number][] = []; + + let asmLines: string[] = this.cleanAsm(utils.splitLines(asmResult)); + const startingLineCount = asmLines.length; + + if (filters.commentOnly) { + const commentRe = /^\s*(;.*)$/g; + asmLines = asmLines.flatMap(l => commentRe.test(l) ? [] : [l]); + } + + const result = this.scanLabelsAndMethods(asmLines, filters.labels); + + for (const i in result.labelDef) { + const label = result.labelDef[i]; + labelDefinitions.push([label.name, parseInt(i)]); + } + + for (const i in result.methodDef) { + const method = result.methodDef[i]; + labelDefinitions.push([method, parseInt(i)]); + } + + for (const line in asmLines) { + if (result.labelDef[line] && result.labelDef[line].remove) continue; + + const labels: InlineLabel[] = []; + const label = result.labelUsage[line] || result.methodUsage[line]; + if (label) { + if (result.allAvailable.includes(label.name)) { + labels.push(label); + } + } + + asm.push({ + text: asmLines[line], + source: null, + labels, + }); + } + + let lineOffset = 1; + labelDefinitions = labelDefinitions.sort((a, b) => a[1] < b[1] ? -1 : 1); + + for (const index in labelDefinitions) { + if (result.labelDef[labelDefinitions[index][1]] && + result.labelDef[labelDefinitions[index][1]].remove) { + labelDefinitions[index][1] = -1; + lineOffset--; + continue; + } + + labelDefinitions[index][1] += lineOffset; + } + + const endTime = process.hrtime.bigint(); + return { + asm: asm, + labelDefinitions: Object.fromEntries(labelDefinitions.filter(i => i[1] !== -1)), + parsingTime: ((endTime - startTime) / BigInt(1000000)).toString(), + filteredCount: startingLineCount - asm.length, + }; + } +} diff --git a/lib/compilers/dotnet.ts b/lib/compilers/dotnet.ts index 65d0e9a3a..e3225a2ab 100644 --- a/lib/compilers/dotnet.ts +++ b/lib/compilers/dotnet.ts @@ -27,6 +27,7 @@ import path from 'path'; import fs from 'fs-extra'; /// +import { DotNetAsmParser } from '../asm-parser-dotnet'; import { BaseCompiler } from '../base-compiler'; class DotNetCompiler extends BaseCompiler { @@ -37,6 +38,7 @@ class DotNetCompiler extends BaseCompiler { private clrBuildDir: string; private additionalSources: string; private langVersion: string; + protected asm: DotNetAsmParser; constructor(compilerInfo, env) { super(compilerInfo, env); @@ -48,6 +50,7 @@ class DotNetCompiler extends BaseCompiler { this.clrBuildDir = this.compilerProps(`compiler.${this.compiler.id}.clrDir`); this.additionalSources = this.compilerProps(`compiler.${this.compiler.id}.additionalSources`); this.langVersion = this.compilerProps(`compiler.${this.compiler.id}.langVersion`); + this.asm = new DotNetAsmParser(); } get compilerOptions() { @@ -90,7 +93,6 @@ class DotNetCompiler extends BaseCompiler { ${this.targetFramework} true - enable CompilerExplorer ${this.langVersion} false @@ -160,36 +162,11 @@ class DotNetCompiler extends BaseCompiler { return this.compilerOptions; } - cleanAsm(stdout) { - let cleanedAsm = ''; - - for (const line of stdout) { - if (line.text.startsWith('; Assembly listing for method')) { - // ; Assembly listing for method ConsoleApplication.Program:Main(System.String[]) - // ^ This character is the 31st character in this string. - // `substring` removes the first 30 characters from it and uses the rest as a label. - cleanedAsm = cleanedAsm.concat(line.text.substring(30) + ':\n'); - continue; - } - - if (line.text.startsWith('Emitting R2R PE file')) { - continue; - } - - if (line.text.startsWith(';') && !line.text.startsWith('; Emitting')) { - continue; - } - - cleanedAsm = cleanedAsm.concat(line.text + '\n'); - } - - return cleanedAsm; - } - async runCrossgen2(compiler, execOptions, crossgen2Path, publishPath, dllPath, options, outputPath) { const crossgen2Options = [ crossgen2Path, '-r', path.join(publishPath, '*'), dllPath, '-o', 'CompilerExplorer.r2r.dll', '--codegenopt', 'NgenDisasm=*', '--codegenopt', 'JitDiffableDasm=1', '--parallelism', '1', + '--inputbubble', '--compilebubblegenerics', ].concat(options); const result = await this.exec(compiler, crossgen2Options, execOptions); @@ -199,7 +176,7 @@ class DotNetCompiler extends BaseCompiler { await fs.writeFile( outputPath, - this.cleanAsm(result.stdout), + result.stdout.map(o => o.text).reduce((a, n) => `${a}\n${n}`), ); return result;