Files
compiler-explorer/lib/parsers/asm-parser-ptx.ts
Cliff Burdick 27a89e86bc Fix braces on PTX parser (#8248)
The PTX emitted can sometimes be of the form:
```
{ code
}
```

Later on if the parser sees that the current and last line were closing
and opening braces, it drops both lines. This prevents any single-line
instructions from showing up. This PR changes the logic to make opcodes
on the same line as opening braces valid, and also adds unit tests for
matching.
2025-11-05 12:06:54 -06:00

317 lines
11 KiB
TypeScript

// Copyright (c) 2025, 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 {AsmResultSource, ParsedAsmResult, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces.js';
import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import {PropertyGetter} from '../properties.interfaces.js';
import * as utils from '../utils.js';
import {AsmParser} from './asm-parser.js';
export class PTXAsmParser extends AsmParser {
protected commentOnlyLine: RegExp;
protected emptyLine: RegExp;
protected override directive: RegExp;
protected externFuncDirective: RegExp;
protected paramDirective: RegExp;
protected visibleDirective: RegExp;
protected override fileFind: RegExp;
protected override sourceTag: RegExp;
protected externFuncStart: RegExp;
protected openParen: RegExp;
protected closeParen: RegExp;
protected semicolon: RegExp;
protected ptxDataDeclaration: RegExp;
protected functionStart: RegExp;
protected functionEnd: RegExp;
protected callInstruction: RegExp;
protected functionCallParam: RegExp;
protected functionCallEnd: RegExp;
protected labelLine: RegExp;
constructor(compilerProps?: PropertyGetter) {
super(compilerProps);
this.commentOnlyLine = /^\s*\/\//;
this.emptyLine = /^\s*$/;
this.directive = /^\s*\..*$/;
this.externFuncDirective = /^\s*\.extern\s+\.func/;
this.paramDirective = /^\s*\.param/;
this.visibleDirective = /^\s*\.visible/;
this.fileFind = /^\s*\.file\s+(\d+)\s+"([^"]+)"/;
this.sourceTag = /^\s*\.loc\s+(\d+)\s+(\d+)\s+(\d+)/;
this.externFuncStart = /^\s*\.extern\s+\.func/;
this.openParen = /^\s*\(\s*$/;
this.closeParen = /^\s*\)\s*$/;
this.semicolon = /^\s*;\s*$/;
this.ptxDataDeclaration =
/^\s*\.global\s+\.align\s+\d+\s+\.(b8|b16|b32|b64|u8|u16|u32|u64|s8|s16|s32|s64|f16|f32|f64)/;
this.functionStart = /^\s*\{/;
this.functionEnd = /^\s*\}/;
this.callInstruction = /^\s*call\./;
this.functionCallParam = /^\s*[a-zA-Z_][a-zA-Z0-9_]*,?\s*$/;
this.functionCallEnd = /^\s*\)\s*;\s*$/;
this.labelLine = /^\s*\$?[a-zA-Z_][a-zA-Z0-9_]*:.*$/;
this.hasOpcodeRe = /^\s*\{?\s*(@!?%\w+\s+)?(%[$.A-Z_a-z][\w$.]*\s*=\s*)?[A-Za-z]/;
}
override processAsm(asmResult: string, filters: ParseFiltersAndOutputOptions): ParsedAsmResult {
const startTime = process.hrtime.bigint();
const asm: ParsedAsmResultLine[] = [];
const asmLines = utils.splitLines(asmResult);
const startingLineCount = asmLines.length;
const files = this.parseFiles(asmLines);
let currentSource: AsmResultSource | null = null;
let inExternFuncDeclaration = false;
let externFuncSeenCloseParen = false;
let inFunctionImplementation = false;
let functionBraceDepth = 0;
let inFunctionCall = false;
let callBaseIndent = '';
let braceDepth = 0;
let lineNumber = 0;
let openBraceLineNumber = 0;
let openBraceLineHasOpcode = false;
for (let line of asmLines) {
const newSource = this.processSourceLine(line, files);
if (newSource) {
currentSource = newSource;
}
if (this.externFuncStart.test(line)) {
inExternFuncDeclaration = true;
externFuncSeenCloseParen = false;
} else if (inExternFuncDeclaration && this.closeParen.test(line)) {
externFuncSeenCloseParen = true;
}
if (this.functionStart.test(line)) {
functionBraceDepth++;
if (functionBraceDepth === 1) {
inFunctionImplementation = true;
}
} else if (this.functionEnd.test(line)) {
functionBraceDepth--;
if (functionBraceDepth === 0) {
inFunctionImplementation = false;
}
}
if (this.callInstruction.test(line)) {
inFunctionCall = true;
const match = line.match(/^(\s*)/);
callBaseIndent = match ? match[1] + '\t' : '\t';
} else if (inFunctionCall && this.functionCallEnd.test(line)) {
inFunctionCall = false;
callBaseIndent = '';
}
if (filters.libraryCode && inExternFuncDeclaration) {
if (externFuncSeenCloseParen && this.semicolon.test(line)) {
inExternFuncDeclaration = false;
externFuncSeenCloseParen = false;
}
continue;
}
if (filters.commentOnly) {
if (this.commentOnlyLine.test(line) || this.emptyLine.test(line)) {
continue;
}
const commentIndex = line.indexOf('//');
if (commentIndex > 0) {
line = line.substring(0, commentIndex).trimEnd();
}
}
if (filters.directives && this.shouldSkipPTXDirective(line, inFunctionImplementation)) {
continue;
}
let processedLine = line;
// Never indent labels
if (this.labelLine.test(line)) {
processedLine = line.trim();
} else {
let indentLevel = braceDepth;
// Adjust indent for closing braces - they should be at the same level as opening brace
if (this.functionEnd.test(line)) {
indentLevel = Math.max(0, braceDepth - 1);
}
if (indentLevel > 0) {
const additionalIndent = '\t'.repeat(indentLevel);
processedLine = additionalIndent + line.trim();
} else if (inFunctionCall && !this.functionCallEnd.test(line)) {
processedLine = this.improveCallIndentation(line, callBaseIndent);
}
}
if (this.functionStart.test(line)) {
braceDepth++;
openBraceLineNumber = lineNumber;
openBraceLineHasOpcode = this.hasOpcode(line);
} else if (this.functionEnd.test(line)) {
braceDepth--;
// Remove paired braces with no content (but not if the opening brace line had an opcode)
if (openBraceLineNumber + 1 === lineNumber && !openBraceLineHasOpcode) {
asm.pop();
continue;
}
}
if (filters.trim) {
processedLine = this.applyTrimFilter(processedLine);
}
asm.push({
text: processedLine,
source: this.hasOpcode(line) ? currentSource : null,
labels: [],
});
lineNumber++;
}
const endTime = process.hrtime.bigint();
return {
asm: asm,
labelDefinitions: {},
languageId: 'ptx',
parsingTime: utils.deltaTimeNanoToMili(startTime, endTime),
filteredCount: startingLineCount - asm.length,
};
}
private shouldSkipPTXDirective(line: string, inFunctionImplementation: boolean): boolean {
if (!this.directive.test(line)) {
return false;
}
if (this.externFuncDirective.test(line)) {
return false;
}
if (this.paramDirective.test(line)) {
return inFunctionImplementation;
}
if (this.visibleDirective.test(line)) {
return false;
}
if (this.ptxDataDeclaration.test(line)) {
return false;
}
return true;
}
private processSourceLine(line: string, files: Record<number, string>): AsmResultSource | null {
const locMatch = line.match(this.sourceTag);
if (locMatch) {
const fileNum = Number.parseInt(locMatch[1], 10);
const lineNum = Number.parseInt(locMatch[2], 10);
const columnNum = Number.parseInt(locMatch[3], 10);
const file = files[fileNum];
if (file) {
return {
file: utils.maskRootdir(file),
line: lineNum,
column: columnNum,
mainsource: true,
};
}
}
return null;
}
private improveCallIndentation(line: string, baseIndent: string): string {
const trimmed = line.trim();
if (!trimmed) {
return line;
}
const currentIndent = line.match(/^(\s*)/)?.[1] || '';
if (/^[a-zA-Z_][a-zA-Z0-9_]*,?\s*$/.test(trimmed)) {
return currentIndent + baseIndent.slice(currentIndent.length) + trimmed;
}
if (trimmed === '(') {
return currentIndent + baseIndent.slice(currentIndent.length) + trimmed;
}
if (trimmed === ')' || trimmed === ');') {
const adjustedIndent = baseIndent.slice(0, -1);
return currentIndent + adjustedIndent.slice(currentIndent.length) + trimmed;
}
return line;
}
private applyTrimFilter(line: string): string {
if (line.trim().length === 0) {
return '';
}
// Convert tabs to 2 spaces while preserving the indentation structure
const leadingTabsMatch = line.match(/^(\t*)/);
const leadingTabs = leadingTabsMatch ? leadingTabsMatch[1].length : 0;
const leadingSpaces = ' '.repeat(leadingTabs);
const contentAfterTabs = line.substring(leadingTabs);
const contentWithSpaces = contentAfterTabs.replace(/\t/g, ' ');
// Squash multiple spaces in the content part but preserve the leading indentation
const contentParts = contentWithSpaces.split(/(\s+)/);
const processedContent = contentParts
.map((part, index) => {
if (index === 0) return part;
if (/^\s+$/.test(part)) return ' ';
return part;
})
.join('');
return leadingSpaces + processedContent;
}
}