Refactor AsmParser to extract testable components (#7777)

## Summary
- Extracted source line handling logic into a dedicated
`SourceLineHandler` class that consolidates .loc, .stabs, and .6502
directive parsing
- Extracted label processing logic into a `LabelProcessor` class with
methods for finding used labels and filtering
- Created a `ParsingState` class to manage parsing loop state variables
in a centralized way
- Fully integrated all components into the main `AsmParser` class,
replacing the original complex parsing loop

## Changes Made
- **SourceLineHandler**: Unifies `.loc`, `.d2line`, `.cv_loc`, `.dbg`,
`.stabn`, and 6502 debug directive parsing
- **LabelProcessor**: Handles complex label detection, filtering, and
cleanup logic with MIPS/non-MIPS support
- **ParsingState**: Encapsulates state management during parsing
(inNvccCode, inCustomAssembly, etc.)
- **Integration**: All components work together through well-defined
interfaces

## Verification
-  All 1082+ tests pass, including new subclass compatibility tests
from PR #7779
-  All 670+ filter tests pass, confirming exact behavior preservation
-  Added comprehensive unit tests for all new components (32 tests
total)
-  TypeScript compilation and linting pass
-  No performance regression in core functionality

## Bug Fix Discovered
The refactoring inadvertently **fixes issue #7781** - EWAVR label
detection bug:
- **Before**: EWAVR couldn't find labels in usage contexts like `ldi
r16, HIGH(_data)` due to `labelFindFor()` returning definition regex
- **After**: Now correctly uses `identifierFindRe` to find labels in
usage contexts
- Updated tests to reflect the corrected behavior

## Benefits
- Reduced complexity in the main `processAsm` method (from 180+ lines to
more manageable chunks)
- Extracted highly testable, focused components with single
responsibilities
- Eliminated code duplication between source handling methods
- Centralized state management reduces scattered variable handling
- Maintained full backward compatibility and exact behavior
- Fixed EWAVR label detection bug as a side effect

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Matt Godbolt
2025-06-11 18:22:44 -05:00
committed by GitHub
parent c4bb7f7c68
commit 04972d6aaa
11 changed files with 1463 additions and 491 deletions

View File

@@ -32,12 +32,20 @@ import {
ParsedAsmResultLine,
} from '../../types/asmresult/asmresult.interfaces.js';
import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import {assert, unwrap} from '../assert.js';
import {assert} from '../assert.js';
import {PropertyGetter} from '../properties.interfaces.js';
import * as utils from '../utils.js';
import {IAsmParser} from './asm-parser.interfaces.js';
import {AsmRegex} from './asmregex.js';
import {LabelContext, LabelProcessor} from './label-processor.js';
import {ParsingState} from './parsing-state.js';
import {SourceHandlerContext, SourceLineHandler} from './source-line-handler.js';
function maybeAddBlank(asm: ParsedAsmResultLine[]) {
const lastBlank = asm.length === 0 || asm[asm.length - 1].text === '';
if (!lastBlank) asm.push({text: '', source: null, labels: []});
}
export type ParsingContext = {
files: Record<number, string>;
@@ -48,53 +56,272 @@ export type ParsingContext = {
};
export class AsmParser extends AsmRegex implements IAsmParser {
labelFindNonMips: RegExp;
labelFindMips: RegExp;
mipsLabelDefinition: RegExp;
dataDefn: RegExp;
fileFind: RegExp;
hasOpcodeRe: RegExp;
instructionRe: RegExp;
identifierFindRe: RegExp;
hasNvccOpcodeRe: RegExp;
definesFunction: RegExp;
definesGlobal: RegExp;
definesWeak: RegExp;
definesAlias: RegExp;
indentedLabelDef: RegExp;
assignmentDef: RegExp;
directive: RegExp;
startAppBlock: RegExp;
endAppBlock: RegExp;
startAsmNesting: RegExp;
endAsmNesting: RegExp;
cudaBeginDef: RegExp;
cudaEndDef: RegExp;
binaryHideFuncRe: RegExp | null;
maxAsmLines: number;
asmOpcodeRe: RegExp;
relocationRe: RegExp;
relocDataSymNameRe: RegExp;
lineRe: RegExp;
labelRe: RegExp;
destRe: RegExp;
commentRe: RegExp;
instOpcodeRe: RegExp;
commentOnly: RegExp;
commentOnlyNvcc: RegExp;
sourceTag: RegExp;
sourceD2Tag: RegExp;
sourceCVTag: RegExp;
source6502Dbg: RegExp;
source6502DbgEnd: RegExp;
sourceStab: RegExp;
stdInLooking: RegExp;
endBlock: RegExp;
blockComments: RegExp;
protected sourceLineHandler: SourceLineHandler;
protected labelProcessor: LabelProcessor;
protected parsingState: ParsingState;
protected maxAsmLines: number;
protected labelFindNonMips: RegExp;
protected labelFindMips: RegExp;
protected mipsLabelDefinition: RegExp;
protected dataDefn: RegExp;
protected fileFind: RegExp;
protected hasOpcodeRe: RegExp;
protected instructionRe: RegExp;
protected identifierFindRe: RegExp;
protected hasNvccOpcodeRe: RegExp;
protected definesFunction: RegExp;
protected definesGlobal: RegExp;
protected definesWeak: RegExp;
protected definesAlias: RegExp;
protected indentedLabelDef: RegExp;
protected assignmentDef: RegExp;
protected directive: RegExp;
protected startAppBlock: RegExp;
protected endAppBlock: RegExp;
protected startAsmNesting: RegExp;
protected endAsmNesting: RegExp;
protected cudaBeginDef: RegExp;
protected cudaEndDef: RegExp;
protected binaryHideFuncRe: RegExp | null;
protected asmOpcodeRe: RegExp;
protected relocationRe: RegExp;
protected relocDataSymNameRe: RegExp;
protected lineRe: RegExp;
protected labelRe: RegExp;
protected destRe: RegExp;
protected commentRe: RegExp;
protected instOpcodeRe: RegExp;
protected commentOnly: RegExp;
protected commentOnlyNvcc: RegExp;
protected sourceTag: RegExp;
protected sourceD2Tag: RegExp;
protected sourceCVTag: RegExp;
protected source6502Dbg: RegExp;
protected source6502DbgEnd: RegExp;
protected sourceStab: RegExp;
protected stdInLooking: RegExp;
protected endBlock: RegExp;
protected blockComments: RegExp;
private updateParsingState(line: string, context: ParsingContext) {
if (this.startAppBlock.test(line.trim()) || this.startAsmNesting.test(line.trim())) {
this.parsingState.enterCustomAssembly();
} else if (this.endAppBlock.test(line.trim()) || this.endAsmNesting.test(line.trim())) {
this.parsingState.exitCustomAssembly();
} else {
this.parsingState.setVLIWPacket(this.checkVLIWpacket(line, this.parsingState.inVLIWpacket));
}
this.handleSource(context, line);
this.handleStabs(context, line);
this.handle6502(context, line);
this.parsingState.updateSource(context.source);
if (this.endBlock.test(line) || (this.parsingState.inNvccCode && /}/.test(line))) {
context.source = null;
context.prevLabel = '';
this.parsingState.resetToBlockEnd();
}
}
private shouldSkipDirective(
line: string,
filters: ParseFiltersAndOutputOptions,
context: ParsingContext,
match: RegExpMatchArray | null,
): boolean {
if (this.parsingState.inNvccDef) {
if (this.cudaEndDef.test(line)) this.parsingState.exitNvccDef();
return false;
}
if (!match && filters.directives) {
// Check for directives only if it wasn't a label; the regexp would otherwise misinterpret labels as directives.
if (this.dataDefn.test(line) && context.prevLabel) {
// We're defining data that's being used somewhere.
return false;
}
// .inst generates an opcode, so does not count as a directive, nor does an alias definition that's used.
if (this.directive.test(line) && !this.instOpcodeRe.test(line) && !this.definesAlias.test(line)) {
return true;
}
}
return false;
}
private processLabelDefinition(
line: string,
filters: ParseFiltersAndOutputOptions,
context: ParsingContext,
asmLines: string[],
labelsUsed: Set<string>,
labelDefinitions: Record<string, number>,
asmLength: number,
): {match: RegExpMatchArray | null; skipLine: boolean} {
let match = line.match(this.labelDef);
if (!match) match = line.match(this.assignmentDef);
if (!match) {
match = line.match(this.cudaBeginDef);
if (match) {
this.parsingState.enterNvccDef();
}
}
if (!match) {
return {match: null, skipLine: false};
}
// It's a label definition. g-as shows local labels as eg: "1: call mcount". We characterize such a label
// as "the label-matching part doesn't equal the whole line" and treat it as used. As a special case,
// consider assignments of the form "symbol = ." to be labels.
if (!labelsUsed.has(match[1]) && match[0] === line && (match[2] === undefined || match[2].trim() === '.')) {
// It's an unused label.
if (filters.labels) {
context.prevLabel = '';
return {match, skipLine: true};
}
} else {
// A used label.
context.prevLabel = match[1];
labelDefinitions[match[1]] = asmLength + 1;
if (!this.parsingState.inNvccDef && !this.parsingState.inNvccCode && filters.libraryCode) {
context.prevLabelIsUserFunction = this.isUserFunctionByLookingAhead(
context,
asmLines,
this.parsingState.getCurrentLineIndex(),
);
}
}
return {match, skipLine: false};
}
private processAllLines(
filters: ParseFiltersAndOutputOptions,
context: ParsingContext,
asmLines: string[],
labelsUsed: Set<string>,
): {asm: ParsedAsmResultLine[]; labelDefinitions: Record<string, number>} {
const asm: ParsedAsmResultLine[] = [];
const labelDefinitions: Record<string, number> = {};
for (let line of this.parsingState) {
if (line.trim() === '') {
maybeAddBlank(asm);
continue;
}
this.updateParsingState(line, context);
if (this.shouldSkipLibraryCode(filters, context, asm, labelDefinitions)) {
continue;
}
if (this.shouldSkipCommentOnlyLine(filters, line)) {
continue;
}
if (this.parsingState.isInCustomAssembly()) line = this.fixLabelIndentation(line);
const labelResult = this.processLabelDefinition(
line,
filters,
context,
asmLines,
labelsUsed,
labelDefinitions,
asm.length,
);
const match = labelResult.match;
if (labelResult.skipLine) {
continue;
}
if (this.shouldSkipDirective(line, filters, context, match)) {
continue;
}
line = utils.expandTabs(line);
const text = AsmRegex.filterAsmLine(line, filters);
const labelsInLine = match ? [] : this.getUsedLabelsInLine(text);
asm.push({
text: text,
source: this.hasOpcode(line, this.parsingState.inNvccCode, this.parsingState.inVLIWpacket)
? context.source || null
: null,
labels: labelsInLine,
});
}
return {asm, labelDefinitions};
}
private shouldSkipCommentOnlyLine(filters: ParseFiltersAndOutputOptions, line: string): boolean {
return Boolean(
filters.commentOnly &&
((this.commentOnly.test(line) && !this.parsingState.inNvccCode) ||
(this.commentOnlyNvcc.test(line) && this.parsingState.inNvccCode)),
);
}
private shouldSkipLibraryCode(
filters: ParseFiltersAndOutputOptions,
context: ParsingContext,
asm: ParsedAsmResultLine[],
labelDefinitions: Record<string, number>,
): boolean {
// Only filter library code if user enabled it AND we're not currently in a user function
const doLibraryFilterCheck = filters.libraryCode && !context.prevLabelIsUserFunction;
// Don't skip if any of these conditions indicate this is user code or filtering is disabled
if (
!doLibraryFilterCheck || // Library filtering disabled or we're in user function
this.parsingState.lastOwnSource || // We recently processed user source code
!context.source || // No source information available
context.source.file === null || // Main source file (user code)
context.source.mainsource // Explicitly marked as main source
) {
// We're in user code, so future labels might need removal if we transition to library code
this.parsingState.setMayRemovePreviousLabel(true);
return false;
}
// We're in library code that should be filtered. Handle "orphaned labels" that precede filtered code.
// When we start filtering library code, we might have just output a label that will now be orphaned.
if (this.parsingState.shouldRemovePreviousLabel() && asm.length > 0) {
const lastLine = asm[asm.length - 1];
const labelDef = lastLine.text ? lastLine.text.match(this.labelDef) : null;
if (labelDef) {
// Last line was a label - it's now orphaned, so remove it retroactively
asm.pop();
this.parsingState.setKeepInlineCode(false);
delete labelDefinitions[labelDef[1]];
} else {
// Last line wasn't a label - there's user code mixed in, so keep showing library code
this.parsingState.setKeepInlineCode(true);
}
// Don't try to remove labels again until we transition back to user code
this.parsingState.setMayRemovePreviousLabel(false);
}
// Skip this line unless we determined there's user code mixed in (keepInlineCode=true)
return !this.parsingState.shouldKeepInlineCode();
}
constructor(compilerProps?: PropertyGetter) {
super();
this.sourceLineHandler = new SourceLineHandler();
this.labelProcessor = new LabelProcessor();
this.parsingState = new ParsingState({}, null, '', false, false, []);
this.labelFindNonMips = /[.A-Z_a-z][\w$.]*/g;
// MIPS labels can start with a $ sign, but other assemblers use $ to mean literal.
this.labelFindMips = /[$.A-Z_a-z][\w$.]*/g;
@@ -144,8 +371,7 @@ export class AsmParser extends AsmRegex implements IAsmParser {
this.lineRe = /^(\/[^:]+):(?<line>\d+).*/;
}
// labelRe is made very greedy as it's also used with demangled objdump output (eg. it can have c++ template
// with <>).
// labelRe is made very greedy as it's also used with demangled objdump output (eg. it can have c++ template with <>).
this.labelRe = /^([\da-f]+)\s+<(.+)>:$/;
this.destRe = /\s([\da-f]+)\s+<([^+>]+)(\+0x[\da-f]+)?>$/;
this.commentRe = /[#;]/;
@@ -189,190 +415,66 @@ export class AsmParser extends AsmRegex implements IAsmParser {
return this.hasOpcodeRe.test(line);
}
private createLabelContext(): LabelContext {
return {
hasOpcode: this.hasOpcode.bind(this),
checkVLIWpacket: this.checkVLIWpacket.bind(this),
labelDef: this.labelDef,
dataDefn: this.dataDefn,
commentRe: this.commentRe,
instructionRe: this.instructionRe,
identifierFindRe: this.identifierFindRe,
definesGlobal: this.definesGlobal,
definesWeak: this.definesWeak,
definesAlias: this.definesAlias,
definesFunction: this.definesFunction,
cudaBeginDef: this.cudaBeginDef,
startAppBlock: this.startAppBlock,
endAppBlock: this.endAppBlock,
startAsmNesting: this.startAsmNesting,
endAsmNesting: this.endAsmNesting,
mipsLabelDefinition: this.mipsLabelDefinition,
labelFindNonMips: this.labelFindNonMips,
labelFindMips: this.labelFindMips,
fixLabelIndentation: this.fixLabelIndentation.bind(this),
};
}
labelFindFor(asmLines: string[]) {
const isMips = _.any(asmLines, line => this.mipsLabelDefinition.test(line));
return isMips ? this.labelFindMips : this.labelFindNonMips;
return this.labelProcessor.getLabelFind(asmLines, this.createLabelContext());
}
findUsedLabels(asmLines: string[], filterDirectives?: boolean): Set<string> {
const labelsUsed: Set<string> = new Set();
const weakUsages: Map<string, Set<string>> = new Map();
function markWeak(fromLabel: string, toLabel: string) {
if (!weakUsages.has(fromLabel)) weakUsages.set(fromLabel, new Set());
unwrap(weakUsages.get(fromLabel)).add(toLabel);
}
const labelFind = this.labelFindFor(asmLines);
// The current label set is the set of labels all pointing at the current code, so:
// foo:
// bar:
// add r0, r0, #1
// in this case [foo, bar] would be the label set for the add instruction.
let currentLabelSet: string[] = [];
let inLabelGroup = false;
let inCustomAssembly = 0;
const startBlock = /\.cfi_startproc/;
const endBlock = /\.cfi_endproc/;
let inFunction = false;
let inNvccCode = false;
let inVLIWpacket = false;
let definingAlias: string | undefined;
// Scan through looking for definite label usages (ones used by opcodes), and ones that are weakly used: that
// is, their use is conditional on another label. For example:
// .foo: .string "moo"
// .baz: .quad .foo
// mov eax, .baz
// In this case, the '.baz' is used by an opcode, and so is strongly used.
// The '.foo' is weakly used by .baz.
// Also, if we have random data definitions within a block of a function (between cfi_startproc and
// cfi_endproc), we assume they are strong usages. This covers things like jump tables embedded in ARM code.
// See https://github.com/compiler-explorer/compiler-explorer/issues/2788
for (let line of asmLines) {
if (this.startAppBlock.test(line.trim()) || this.startAsmNesting.test(line.trim())) {
inCustomAssembly++;
} else if (this.endAppBlock.test(line.trim()) || this.endAsmNesting.test(line.trim())) {
inCustomAssembly--;
} else if (startBlock.test(line)) {
inFunction = true;
} else if (endBlock.test(line)) {
inFunction = false;
} else if (this.cudaBeginDef.test(line)) {
inNvccCode = true;
} else {
inVLIWpacket = this.checkVLIWpacket(line, inVLIWpacket);
}
if (inCustomAssembly > 0) line = this.fixLabelIndentation(line);
let match = line.match(this.labelDef);
if (match) {
if (inLabelGroup) currentLabelSet.push(match[1]);
else currentLabelSet = [match[1]];
inLabelGroup = true;
if (definingAlias) {
// If we're defining an alias, then any labels in this group are weakly used by the alias.
markWeak(definingAlias, match[1]);
}
} else {
if (inLabelGroup) {
inLabelGroup = false;
// Once we exit the label group after an alias, we're no longer defining an alias.
definingAlias = undefined;
}
}
match = line.match(this.definesGlobal);
if (!match) match = line.match(this.definesWeak);
if (!match) match = line.match(this.cudaBeginDef);
if (match) labelsUsed.add(match[1]);
const definesAlias = line.match(this.definesAlias);
if (definesAlias) {
// We are defining an alias for match[1]; so the next label definition is the _same_ as this.
definingAlias = definesAlias[1];
}
const definesFunction = line.match(this.definesFunction);
if (!definesFunction && (!line || line[0] === '.')) continue;
match = line.match(labelFind);
if (!match) continue;
if (!filterDirectives || this.hasOpcode(line, inNvccCode, inVLIWpacket) || definesFunction) {
// Only count a label as used if it's used by an opcode, or else we're not filtering directives.
for (const label of match) labelsUsed.add(label);
} else {
// If we have a current label, then any subsequent opcode or data definition's labels are referred to
// weakly by that label.
const isDataDefinition = this.dataDefn.test(line);
const isOpcode = this.hasOpcode(line, inNvccCode, inVLIWpacket);
if (isDataDefinition || isOpcode) {
if (inFunction && isDataDefinition) {
// Data definitions in the middle of code should be treated as if they were used strongly.
for (const label of match) labelsUsed.add(label);
} else {
for (const currentLabel of currentLabelSet) {
for (const label of match) markWeak(currentLabel, label);
}
}
}
}
}
// Now follow the chains of used labels, marking any weak references they refer to as also used. We recursively
// follow the newly-strong references along the path until we hit something that's already marked as used.
const recurseMarkUsed = (label: string) => {
labelsUsed.add(label);
const usages = weakUsages.get(label);
if (!usages) return;
for (const nowUsed of usages) {
if (!labelsUsed.has(nowUsed)) recurseMarkUsed(nowUsed);
}
};
// Iterate over a copy of the initial used labels, as the set will be modified during iteration.
for (const label of new Set(labelsUsed)) recurseMarkUsed(label);
return labelsUsed;
return this.labelProcessor.findUsedLabels(asmLines, filterDirectives || false, this.createLabelContext());
}
parseFiles(asmLines: string[]) {
const files: Record<number, string> = {};
for (const line of asmLines) {
const match = line.match(this.fileFind);
if (match) {
const lineNum = Number.parseInt(match[1]);
if (match[4] && !line.includes('.cv_file')) {
// Clang-style file directive '.file X "dir" "filename"'
if (match[4].startsWith('/')) {
files[lineNum] = match[4];
} else {
files[lineNum] = match[2] + '/' + match[4];
}
if (!match) continue;
const lineNum = Number.parseInt(match[1]);
if (match[4] && !line.includes('.cv_file')) {
// Clang-style file directive '.file X "dir" "filename"'
if (match[4].startsWith('/')) {
files[lineNum] = match[4];
} else {
files[lineNum] = match[2];
files[lineNum] = match[2] + '/' + match[4];
}
} else {
files[lineNum] = match[2];
}
}
return files;
}
// Remove labels which do not have a definition.
removeLabelsWithoutDefinition(asm: ParsedAsmResultLine[], labelDefinitions: Record<string, number>) {
for (const obj of asm) {
if (obj.labels) {
obj.labels = obj.labels.filter(label => labelDefinitions[label.target || label.name]);
}
}
this.labelProcessor.removeLabelsWithoutDefinition(asm, labelDefinitions);
}
// Get labels which are used in the given line.
getUsedLabelsInLine(line: string): AsmResultLabel[] {
const labelsInLine: AsmResultLabel[] = [];
// Strip any comments
const instruction = line.split(this.commentRe, 1)[0];
// Remove the instruction.
const params = instruction.replace(this.instructionRe, '');
const removedCol = instruction.length - params.length + 1;
params.replace(this.identifierFindRe, (symbol, target, index) => {
const startCol = removedCol + index;
const label: AsmResultLabel = {
name: symbol,
range: {
startCol: startCol,
endCol: startCol + symbol.length,
},
};
if (target !== symbol) {
label.target = target;
}
labelsInLine.push(label);
return symbol;
});
return labelsInLine;
return this.labelProcessor.getUsedLabelsInLine(line, this.createLabelContext());
}
protected isUserFunctionByLookingAhead(context: ParsingContext, asmLines: string[], idxFrom: number): boolean {
@@ -401,103 +503,36 @@ export class AsmParser extends AsmRegex implements IAsmParser {
}
protected handleSource(context: ParsingContext, line: string) {
let match = line.match(this.sourceTag);
if (match) {
const file = utils.maskRootdir(context.files[Number.parseInt(match[1])]);
const sourceLine = Number.parseInt(match[2]);
if (file) {
if (context.dontMaskFilenames) {
context.source = {
file: file,
line: sourceLine,
mainsource: this.stdInLooking.test(file),
};
} else {
context.source = {
file: this.stdInLooking.test(file) ? null : file,
line: sourceLine,
};
}
const sourceCol = Number.parseInt(match[3]);
if (!Number.isNaN(sourceCol) && sourceCol !== 0) {
context.source.column = sourceCol;
}
} else {
context.source = null;
}
} else {
match = line.match(this.sourceD2Tag);
if (match) {
const sourceLine = Number.parseInt(match[1]);
context.source = {
file: null,
line: sourceLine,
};
} else {
match = line.match(this.sourceCVTag);
if (match) {
// cv_loc reports: function file line column
const sourceLine = Number.parseInt(match[3]);
const file = utils.maskRootdir(context.files[Number.parseInt(match[2])]);
if (context.dontMaskFilenames) {
context.source = {
file: file,
line: sourceLine,
mainsource: this.stdInLooking.test(file),
};
} else {
context.source = {
file: this.stdInLooking.test(file) ? null : file,
line: sourceLine,
};
}
const sourceCol = Number.parseInt(match[4]);
if (!Number.isNaN(sourceCol) && sourceCol !== 0) {
context.source.column = sourceCol;
}
}
}
}
const sourceContext: SourceHandlerContext = {
files: context.files,
dontMaskFilenames: context.dontMaskFilenames,
};
const result = this.sourceLineHandler.processSourceLine(line, sourceContext);
if (result.source !== undefined) context.source = result.source;
if (result.resetPrevLabel) context.prevLabel = '';
}
protected handleStabs(context: ParsingContext, line: string) {
const match = line.match(this.sourceStab);
if (!match) return;
// cf http://www.math.utah.edu/docs/info/stabs_11.html#SEC48
switch (Number.parseInt(match[1])) {
case 68: {
context.source = {file: null, line: Number.parseInt(match[2])};
break;
}
case 132:
case 100: {
context.source = null;
context.prevLabel = '';
break;
}
}
const sourceContext: SourceHandlerContext = {
files: context.files,
dontMaskFilenames: context.dontMaskFilenames,
};
const result = this.sourceLineHandler.processSourceLine(line, sourceContext);
if (result.source !== undefined) context.source = result.source;
if (result.resetPrevLabel) context.prevLabel = '';
}
protected handle6502(context: ParsingContext, line: string) {
const match = line.match(this.source6502Dbg);
if (match) {
const file = utils.maskRootdir(match[1]);
const sourceLine = Number.parseInt(match[2]);
if (context.dontMaskFilenames) {
context.source = {
file: file,
line: sourceLine,
mainsource: this.stdInLooking.test(file),
};
} else {
context.source = {
file: this.stdInLooking.test(file) ? null : file,
line: sourceLine,
};
}
} else if (this.source6502DbgEnd.test(line)) {
context.source = null;
}
const sourceContext: SourceHandlerContext = {
files: context.files,
dontMaskFilenames: context.dontMaskFilenames,
};
const result = this.sourceLineHandler.processSourceLine(line, sourceContext);
if (result.source !== undefined) context.source = result.source;
if (result.resetPrevLabel) context.prevLabel = '';
}
processAsm(asmResult: string, filters: ParseFiltersAndOutputOptions): ParsedAsmResult {
@@ -510,178 +545,24 @@ export class AsmParser extends AsmRegex implements IAsmParser {
asmResult = asmResult.replace(this.blockComments, '');
}
const asm: ParsedAsmResultLine[] = [];
const labelDefinitions: Record<string, number> = {};
let asmLines = utils.splitLines(asmResult);
const startingLineCount = asmLines.length;
if (filters.preProcessLines !== undefined) {
asmLines = filters.preProcessLines(asmLines);
}
if (filters.preProcessLines) asmLines = filters.preProcessLines(asmLines);
const labelsUsed = this.findUsedLabels(asmLines, filters.directives);
let mayRemovePreviousLabel = true;
let keepInlineCode = false;
let lastOwnSource: AsmResultSource | undefined | null;
const files = this.parseFiles(asmLines);
this.parsingState = new ParsingState(files, null, '', false, filters.dontMaskFilenames || false, asmLines);
const context: ParsingContext = {
files: this.parseFiles(asmLines),
files: files,
source: null,
prevLabel: '',
prevLabelIsUserFunction: false,
dontMaskFilenames: filters.dontMaskFilenames || false,
};
function maybeAddBlank() {
const lastBlank = asm.length === 0 || asm[asm.length - 1].text === '';
if (!lastBlank) asm.push({text: '', source: null, labels: []});
}
let inNvccDef = false;
let inNvccCode = false;
let inCustomAssembly = 0;
let inVLIWpacket = false;
let idxLine = 0;
// TODO: Make this function smaller
while (idxLine < asmLines.length) {
let line = asmLines[idxLine];
idxLine++;
if (line.trim() === '') {
maybeAddBlank();
continue;
}
if (this.startAppBlock.test(line.trim()) || this.startAsmNesting.test(line.trim())) {
inCustomAssembly++;
} else if (this.endAppBlock.test(line.trim()) || this.endAsmNesting.test(line.trim())) {
inCustomAssembly--;
} else {
inVLIWpacket = this.checkVLIWpacket(line, inVLIWpacket);
}
this.handleSource(context, line);
this.handleStabs(context, line);
this.handle6502(context, line);
if (context.source && (context.source.file === null || context.source.mainsource)) {
lastOwnSource = context.source;
}
if (this.endBlock.test(line) || (inNvccCode && /}/.test(line))) {
context.source = null;
context.prevLabel = '';
lastOwnSource = null;
}
const doLibraryFilterCheck = filters.libraryCode && !context.prevLabelIsUserFunction;
if (
doLibraryFilterCheck &&
!lastOwnSource &&
context.source &&
context.source.file !== null &&
!context.source.mainsource
) {
if (mayRemovePreviousLabel && asm.length > 0) {
const lastLine = asm[asm.length - 1];
const labelDef = lastLine.text ? lastLine.text.match(this.labelDef) : null;
if (labelDef) {
asm.pop();
keepInlineCode = false;
delete labelDefinitions[labelDef[1]];
} else {
keepInlineCode = true;
}
mayRemovePreviousLabel = false;
}
if (!keepInlineCode) {
continue;
}
} else {
mayRemovePreviousLabel = true;
}
if (
filters.commentOnly &&
((this.commentOnly.test(line) && !inNvccCode) || (this.commentOnlyNvcc.test(line) && inNvccCode))
) {
continue;
}
if (inCustomAssembly > 0) line = this.fixLabelIndentation(line);
let match = line.match(this.labelDef);
if (!match) match = line.match(this.assignmentDef);
if (!match) {
match = line.match(this.cudaBeginDef);
if (match) {
inNvccDef = true;
inNvccCode = true;
}
}
if (match) {
// It's a label definition.
// g-as shows local labels as eg: "1: call mcount". We characterize such a label as "the
// label-matching part doesn't equal the whole line" and treat it as used. As a special case, consider
// assignments of the form "symbol = ." to be labels.
if (
!labelsUsed.has(match[1]) &&
match[0] === line &&
(match[2] === undefined || match[2].trim() === '.')
) {
// It's an unused label.
if (filters.labels) {
context.prevLabel = '';
continue;
}
} else {
// A used label.
context.prevLabel = match[1];
labelDefinitions[match[1]] = asm.length + 1;
if (!inNvccDef && !inNvccCode && filters.libraryCode) {
context.prevLabelIsUserFunction = this.isUserFunctionByLookingAhead(context, asmLines, idxLine);
}
}
}
if (inNvccDef) {
if (this.cudaEndDef.test(line)) inNvccDef = false;
} else if (!match && filters.directives) {
// Check for directives only if it wasn't a label; the regexp would otherwise misinterpret labels as
// directives.
if (this.dataDefn.test(line) && context.prevLabel) {
// We're defining data that's being used somewhere.
} else {
// .inst generates an opcode, so does not count as a directive, nor does an alias definition that's
// used.
if (this.directive.test(line) && !this.instOpcodeRe.test(line) && !this.definesAlias.test(line)) {
continue;
}
}
}
line = utils.expandTabs(line);
const text = AsmRegex.filterAsmLine(line, filters);
const labelsInLine = match ? [] : this.getUsedLabelsInLine(text);
asm.push({
text: text,
source: this.hasOpcode(line, inNvccCode, inVLIWpacket) ? context.source || null : null,
labels: labelsInLine,
});
}
const {asm, labelDefinitions} = this.processAllLines(filters, context, asmLines, labelsUsed);
this.removeLabelsWithoutDefinition(asm, labelDefinitions);
@@ -696,10 +577,7 @@ export class AsmParser extends AsmRegex implements IAsmParser {
fixLabelIndentation(line: string) {
const match = line.match(this.indentedLabelDef);
if (match) {
return line.replace(/^\s+/, '');
}
return line;
return match ? line.replace(/^\s+/, '') : line;
}
isUserFunction(func: string) {
@@ -727,9 +605,7 @@ export class AsmParser extends AsmRegex implements IAsmParser {
};
}
if (filters.preProcessBinaryAsmLines !== undefined) {
asmLines = filters.preProcessBinaryAsmLines(asmLines);
}
if (filters.preProcessBinaryAsmLines) asmLines = filters.preProcessBinaryAsmLines(asmLines);
for (const line of asmLines) {
const labelsInLine: AsmResultLabel[] = [];

View File

@@ -0,0 +1,237 @@
// 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 {AsmResultLabel, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces.js';
class FindLabelsState {
public labelsUsed = new Set<string>();
public weakUsages = new Map<string, Set<string>>();
public currentLabelSet: string[] = [];
public inLabelGroup = false;
public inCustomAssembly = 0;
public inFunction = false;
public inNvccCode = false;
public inVLIWpacket = false;
public definingAlias: string | undefined;
markWeak(fromLabel: string, toLabel: string): void {
const usageSet = this.weakUsages.get(fromLabel) ?? new Set<string>();
if (!this.weakUsages.has(fromLabel)) this.weakUsages.set(fromLabel, usageSet);
usageSet.add(toLabel);
}
enterLabelGroup(label: string): void {
if (this.inLabelGroup) {
this.currentLabelSet.push(label);
} else {
this.currentLabelSet = [label];
}
this.inLabelGroup = true;
if (this.definingAlias) {
this.markWeak(this.definingAlias, label);
}
}
exitLabelGroup(): void {
this.inLabelGroup = false;
this.definingAlias = undefined;
}
}
export type LabelContext = {
hasOpcode: (line: string, inNvccCode?: boolean, inVLIWpacket?: boolean) => boolean;
checkVLIWpacket: (line: string, inVLIWpacket: boolean) => boolean;
labelDef: RegExp;
dataDefn: RegExp;
commentRe: RegExp;
instructionRe: RegExp;
identifierFindRe: RegExp;
definesGlobal: RegExp;
definesWeak: RegExp;
definesAlias: RegExp;
definesFunction: RegExp;
cudaBeginDef: RegExp;
startAppBlock: RegExp;
endAppBlock: RegExp;
startAsmNesting: RegExp;
endAsmNesting: RegExp;
mipsLabelDefinition: RegExp;
labelFindNonMips: RegExp;
labelFindMips: RegExp;
fixLabelIndentation: (line: string) => string;
};
export class LabelProcessor {
getLabelFind(asmLines: string[], context: LabelContext): RegExp {
const isMips = asmLines.some(line => context.mipsLabelDefinition.test(line));
return isMips ? context.labelFindMips : context.labelFindNonMips;
}
getUsedLabelsInLine(line: string, context: LabelContext): AsmResultLabel[] {
const labelsInLine: AsmResultLabel[] = [];
// Strip any comments
const instruction = line.split(context.commentRe, 1)[0];
// Remove the instruction
const params = instruction.replace(context.instructionRe, '');
const removedCol = instruction.length - params.length + 1;
params.replace(context.identifierFindRe, (symbol, target, index) => {
const startCol = removedCol + index;
const label: AsmResultLabel = {
name: symbol,
range: {
startCol: startCol,
endCol: startCol + symbol.length,
},
};
if (target !== symbol) label.target = target;
labelsInLine.push(label);
return symbol;
});
return labelsInLine;
}
removeLabelsWithoutDefinition(asm: ParsedAsmResultLine[], labelDefinitions: Record<string, number>) {
for (const obj of asm) {
if (obj.labels) {
obj.labels = obj.labels.filter(label => labelDefinitions[label.target || label.name]);
}
}
}
private updateAssemblyContext(line: string, context: LabelContext, state: FindLabelsState): void {
const startBlock = /\.cfi_startproc/;
const endBlock = /\.cfi_endproc/;
const trimmedLine = line.trim();
if (context.startAppBlock.test(trimmedLine) || context.startAsmNesting.test(trimmedLine)) {
state.inCustomAssembly++;
} else if (context.endAppBlock.test(trimmedLine) || context.endAsmNesting.test(trimmedLine)) {
state.inCustomAssembly--;
} else if (startBlock.test(line)) {
state.inFunction = true;
} else if (endBlock.test(line)) {
state.inFunction = false;
} else if (context.cudaBeginDef.test(line)) {
state.inNvccCode = true;
} else {
state.inVLIWpacket = context.checkVLIWpacket(line, state.inVLIWpacket);
}
}
private preprocessLine(originalLine: string, context: LabelContext, state: FindLabelsState): string {
return state.inCustomAssembly > 0 ? context.fixLabelIndentation(originalLine) : originalLine;
}
private processLabelDefinition(line: string, context: LabelContext, state: FindLabelsState): void {
const match = line.match(context.labelDef);
if (match) {
state.enterLabelGroup(match[1]);
} else if (state.inLabelGroup) state.exitLabelGroup();
}
private processGlobalWeakDefinitions(line: string, context: LabelContext, state: FindLabelsState): void {
const match =
line.match(context.definesGlobal) ?? line.match(context.definesWeak) ?? line.match(context.cudaBeginDef);
if (match) state.labelsUsed.add(match[1]);
const definesAlias = line.match(context.definesAlias);
if (definesAlias) state.definingAlias = definesAlias[1];
}
private processLabelUsages(
line: string,
context: LabelContext,
state: FindLabelsState,
filterDirectives: boolean,
labelFind: RegExp,
): void {
const definesFunction = line.match(context.definesFunction);
if (!definesFunction && (!line || line[0] === '.')) return;
const match = line.match(labelFind);
if (!match) return;
if (!filterDirectives || context.hasOpcode(line, state.inNvccCode, state.inVLIWpacket) || definesFunction) {
for (const label of match) state.labelsUsed.add(label);
} else {
const isDataDefinition = context.dataDefn.test(line);
const isOpcode = context.hasOpcode(line, state.inNvccCode, state.inVLIWpacket);
if (isDataDefinition || isOpcode) {
if (state.inFunction && isDataDefinition) {
for (const label of match) state.labelsUsed.add(label);
} else {
for (const currentLabel of state.currentLabelSet) {
for (const label of match) state.markWeak(currentLabel, label);
}
}
}
}
}
private resolveWeakUsages(state: FindLabelsState): void {
const recurseMarkUsed = (label: string) => {
state.labelsUsed.add(label);
const usages = state.weakUsages.get(label);
if (!usages) return;
for (const nowUsed of usages) {
if (!state.labelsUsed.has(nowUsed)) recurseMarkUsed(nowUsed);
}
};
// Create a snapshot of labelsUsed to avoid processing labels added during recursion
for (const label of new Set(state.labelsUsed)) recurseMarkUsed(label);
}
findUsedLabels(asmLines: string[], filterDirectives: boolean, context: LabelContext): Set<string> {
const state = new FindLabelsState();
const labelFind = this.getLabelFind(asmLines, context);
for (const originalLine of asmLines) {
this.updateAssemblyContext(originalLine, context, state);
const line = this.preprocessLine(originalLine, context, state);
this.processLabelDefinition(line, context, state);
this.processGlobalWeakDefinitions(line, context, state);
this.processLabelUsages(line, context, state, filterDirectives, labelFind);
}
this.resolveWeakUsages(state);
return state.labelsUsed;
}
isLabelUsed(labelName: string, usedLabels: Set<string>, match: RegExpMatchArray, line: string): boolean {
return usedLabels.has(labelName) && (match[0] !== line || (match[2] !== undefined && match[2].trim() !== '.'));
}
shouldFilterLabel(match: RegExpMatchArray, line: string, labelsUsed: Set<string>, filtersLabels: boolean): boolean {
if (!filtersLabels) return false;
return !labelsUsed.has(match[1]) && match[0] === line && (match[2] === undefined || match[2].trim() === '.');
}
}

View File

@@ -0,0 +1,131 @@
// 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
// 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} from '../../types/asmresult/asmresult.interfaces.js';
export class ParsingState {
public mayRemovePreviousLabel = true;
public keepInlineCode = false;
public lastOwnSource: AsmResultSource | undefined | null = null;
public inNvccDef = false;
public inNvccCode = false;
public inCustomAssembly = 0;
public inVLIWpacket = false;
private currentIndex = 0;
constructor(
public files: Record<number, string>,
public source: AsmResultSource | undefined | null,
public prevLabel: string,
public prevLabelIsUserFunction: boolean,
public dontMaskFilenames: boolean,
private asmLines: string[],
) {}
getCurrentLineIndex(): number {
return this.currentIndex;
}
*[Symbol.iterator](): Generator<string, void, unknown> {
while (this.currentIndex < this.asmLines.length) {
const line = this.asmLines[this.currentIndex];
this.currentIndex++;
yield line;
}
}
updateSource(newSource: AsmResultSource | null | undefined) {
this.source = newSource;
if (newSource?.file === null || newSource?.mainsource) {
this.lastOwnSource = newSource;
}
}
resetToBlockEnd() {
this.source = null;
this.prevLabel = '';
this.lastOwnSource = null;
}
enterCustomAssembly() {
this.inCustomAssembly++;
}
exitCustomAssembly() {
this.inCustomAssembly--;
}
isInCustomAssembly(): boolean {
return this.inCustomAssembly > 0;
}
setVLIWPacket(inVLIWpacket: boolean) {
this.inVLIWpacket = inVLIWpacket;
}
enterNvccDef() {
this.inNvccDef = true;
this.inNvccCode = true;
}
exitNvccDef() {
this.inNvccDef = false;
}
shouldFilterLibraryCode(filters: {libraryCode?: boolean}): boolean {
const isLibraryCodeFilterEnabled = Boolean(filters.libraryCode);
const isNotUserFunction = !this.prevLabelIsUserFunction;
const hasNoLastOwnSource = !this.lastOwnSource;
const hasSourceFile = Boolean(this.source?.file);
const isNotMainSource = !this.source?.mainsource;
return (
isLibraryCodeFilterEnabled && isNotUserFunction && hasNoLastOwnSource && hasSourceFile && isNotMainSource
);
}
shouldRemovePreviousLabel(): boolean {
return this.mayRemovePreviousLabel;
}
setMayRemovePreviousLabel(value: boolean) {
this.mayRemovePreviousLabel = value;
}
setKeepInlineCode(value: boolean) {
this.keepInlineCode = value;
}
shouldKeepInlineCode(): boolean {
return this.keepInlineCode;
}
updatePrevLabel(label: string, isUserFunction = false) {
this.prevLabel = label;
this.prevLabelIsUserFunction = isUserFunction;
}
clearPrevLabel() {
this.prevLabel = '';
this.prevLabelIsUserFunction = false;
}
}

View File

@@ -0,0 +1,173 @@
// 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} from '../../types/asmresult/asmresult.interfaces.js';
import * as utils from '../utils.js';
export type SourceHandlerContext = {
files: Record<number, string>;
dontMaskFilenames: boolean;
};
// STAB debugging format constants
// See: http://www.math.utah.edu/docs/info/stabs_11.html#SEC48
const STAB_N_SLINE = 68; // Source line: maps line numbers to addresses
const STAB_N_SO = 100; // Source file: marks beginning of source file debugging info
const STAB_N_SOL = 132; // Included file: tracks #included files
export class SourceLineHandler {
private sourceTag: RegExp;
private sourceD2Tag: RegExp;
private sourceCVTag: RegExp;
private source6502Dbg: RegExp;
private source6502DbgEnd: RegExp;
private sourceStab: RegExp;
private stdInLooking: RegExp;
constructor() {
this.sourceTag = /^\s*\.loc\s+(\d+)\s+(\d+)\s+(.*)/;
this.sourceD2Tag = /^\s*\.d2line\s+(\d+),?\s*(\d*).*/;
this.sourceCVTag = /^\s*\.cv_loc\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+).*/;
this.source6502Dbg = /^\s*\.dbg\s+line,\s*"([^"]+)",\s*(\d+)/;
this.source6502DbgEnd = /^\s*\.dbg\s+line[^,]/;
this.sourceStab = /^\s*\.stabn\s+(\d+),0,(\d+),.*/;
this.stdInLooking = /<stdin>|^-$|example\.[^/]+$|<source>/;
}
private createSource(file: string, line: number, context: SourceHandlerContext, column?: number): AsmResultSource {
const isMainSource = this.stdInLooking.test(file);
const source: AsmResultSource = context.dontMaskFilenames
? {
file,
line,
mainsource: isMainSource,
}
: {
file: isMainSource ? null : file,
line,
};
if (column !== undefined && !Number.isNaN(column) && column !== 0) {
source.column = column;
}
return source;
}
handleSourceTag(line: string, context: SourceHandlerContext): AsmResultSource | null {
const match = line.match(this.sourceTag);
if (!match) return null;
const file = utils.maskRootdir(context.files[Number.parseInt(match[1])]);
const sourceLine = Number.parseInt(match[2]);
if (!file) return null;
return this.createSource(file, sourceLine, context, Number.parseInt(match[3]));
}
handleD2Tag(line: string): AsmResultSource | null {
const match = line.match(this.sourceD2Tag);
if (!match) return null;
return {
file: null,
line: Number.parseInt(match[1]),
};
}
handleCVTag(line: string, context: SourceHandlerContext): AsmResultSource | null {
const match = line.match(this.sourceCVTag);
if (!match) return null;
const sourceLine = Number.parseInt(match[3]);
const file = utils.maskRootdir(context.files[Number.parseInt(match[2])]);
return this.createSource(file, sourceLine, context, Number.parseInt(match[4]));
}
handle6502Debug(line: string, context: SourceHandlerContext): AsmResultSource | null {
if (this.source6502DbgEnd.test(line)) {
return null;
}
const match = line.match(this.source6502Dbg);
if (!match) return null;
const file = utils.maskRootdir(match[1]);
const sourceLine = Number.parseInt(match[2]);
return this.createSource(file, sourceLine, context);
}
handleStabs(line: string): AsmResultSource | null | undefined {
const match = line.match(this.sourceStab);
if (!match) return undefined;
// cf http://www.math.utah.edu/docs/info/stabs_11.html#SEC48
switch (Number.parseInt(match[1])) {
case STAB_N_SLINE:
return {file: null, line: Number.parseInt(match[2])};
case STAB_N_SO:
case STAB_N_SOL:
return null;
default:
return undefined;
}
}
processSourceLine(
line: string,
context: SourceHandlerContext,
): {
source: AsmResultSource | null | undefined;
resetPrevLabel: boolean;
} {
// Try each source handler in order
const handlers: Array<() => AsmResultSource | null> = [
() => this.handleSourceTag(line, context),
() => this.handleD2Tag(line),
() => this.handleCVTag(line, context),
() => this.handle6502Debug(line, context),
];
for (const handler of handlers) {
const source = handler();
if (source) {
return {source, resetPrevLabel: false};
}
}
// Special handling for stabs
const stabResult = this.handleStabs(line);
if (stabResult !== undefined) {
const stabMatch = line.match(this.sourceStab);
const resetPrevLabel =
stabResult === null && (stabMatch?.[1] === String(STAB_N_SOL) || stabMatch?.[1] === String(STAB_N_SO));
return {source: stabResult, resetPrevLabel};
}
return {source: undefined, resetPrevLabel: false};
}
}

View File

@@ -62,7 +62,7 @@ describe('AsmParser subclass compatibility', () => {
expect(usedLabels.has('_start')).toBe(true);
});
it('should demonstrate EWAVR labelFindFor bug prevents finding label usage', () => {
it('should show EWAVR label finding now works correctly after refactoring', () => {
const asmLines = [
'_data: .word 0x1234',
'_main:',
@@ -73,14 +73,16 @@ describe('AsmParser subclass compatibility', () => {
];
const usedLabels = initializeParserAndFindLabels(AsmEWAVRParser, [properties.fakeProps({})], asmLines);
// Bug: finds no labels because labelFindFor() returns definition regex
expect(usedLabels.size).toBe(0);
expect(usedLabels.has('_data')).toBe(false);
expect(usedLabels.has('_subroutine')).toBe(false);
expect(usedLabels.has('_main')).toBe(false);
// Fixed: now correctly finds labels in usage contexts after refactoring
expect(usedLabels.has('_data')).toBe(true);
expect(usedLabels.has('_subroutine')).toBe(true);
expect(usedLabels.has('_main')).toBe(true);
expect(usedLabels.has('HIGH')).toBe(true);
expect(usedLabels.has('LOW')).toBe(true);
// Verify that specific expected labels are found rather than checking exact count
// The bug is that EWAVR's labelFindFor looks for lines ending with ':'
// instead of finding label references in instructions
// The refactoring fixed the issue where EWAVR's labelFindFor returned definition regex
// Now it uses the base class identifierFindRe for finding label references
});
it('should show base class finds all identifier-like tokens as potential labels', () => {

View File

@@ -151,19 +151,20 @@ describe('AsmEWAVRParser', () => {
expect(result.labelDefinitions).toBeUndefined();
});
it('should demonstrate EWAVR labelFindFor bug with label usage detection', () => {
it('should correctly find labels in usage contexts after refactoring fix', () => {
const asmLines = ['ldi r16, HIGH(_data)', 'ldi r17, LOW(_data)', 'call _subroutine', 'rjmp _loop'];
const usedLabels = parser.findUsedLabels(asmLines, true);
// EWAVR labelFindFor() bug: returns definition regex instead of usage regex
// This causes findUsedLabels to find no labels in usage contexts
expect(usedLabels.has('_data')).toBe(false);
expect(usedLabels.has('_subroutine')).toBe(false);
expect(usedLabels.has('_loop')).toBe(false);
expect(usedLabels.size).toBe(0);
// After refactoring: correctly finds labels in usage contexts
expect(usedLabels.has('_data')).toBe(true);
expect(usedLabels.has('_subroutine')).toBe(true);
expect(usedLabels.has('_loop')).toBe(true);
expect(usedLabels.has('HIGH')).toBe(true); // Ensure HIGH is included
expect(usedLabels.has('LOW')).toBe(true); // Ensure LOW is included
// Verify we found the expected labels rather than checking exact count
// The regex is designed for definitions (with colons) not usage
// The labelFindFor regex is still for definitions (with colons)
const labelFindRegex = parser.labelFindFor();
expect(labelFindRegex.test('_data:')).toBe(true); // Matches definitions
expect(labelFindRegex.test('_data')).toBe(false); // Doesn't match usage

View File

@@ -0,0 +1,143 @@
// 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 {describe, expect, it} from 'vitest';
import {ParsedAsmResultLine} from '../types/asmresult/asmresult.interfaces.js';
import {LabelContext, LabelProcessor} from '../lib/parsers/label-processor.js';
describe('LabelProcessor tests', () => {
const processor = new LabelProcessor();
const mockContext: LabelContext = {
hasOpcode: (line: string) => /^\s*[A-Za-z]/.test(line),
checkVLIWpacket: () => false,
labelDef: /^([.A-Z_a-z][\w$.]*):$/,
dataDefn: /^\s*\.(ascii|byte|word|quad)/,
commentRe: /[#;]/,
instructionRe: /^\s*[A-Za-z]+/,
identifierFindRe: /([$.@A-Z_a-z]\w*)(?:@\w+)*/g,
definesGlobal: /^\s*\.global\s*([.A-Z_a-z][\w$.]*)/,
definesWeak: /^\s*\.weak\s*([.A-Z_a-z][\w$.]*)/,
definesAlias: /^\s*\.set\s*([.A-Z_a-z][\w$.]*\s*),\s*\.\s*(\+\s*0)?$/,
definesFunction: /^\s*\.type.*,\s*[#%@]function$/,
cudaBeginDef: /\.(entry|func)\s+(?:\([^)]*\)\s*)?([$.A-Z_a-z][\w$.]*)$/,
startAppBlock: /^#APP.*$/,
endAppBlock: /^#NO_APP.*$/,
startAsmNesting: /^# Begin ASM.*$/,
endAsmNesting: /^# End ASM.*$/,
mipsLabelDefinition: /^\$[\w$.]+:/,
labelFindNonMips: /[.A-Z_a-z][\w$.]*/g,
labelFindMips: /[$.A-Z_a-z][\w$.]*/g,
fixLabelIndentation: (line: string) => line.replace(/^\s+/, ''),
};
describe('getLabelFind', () => {
it('should return MIPS regex for MIPS assembly', () => {
const asmLines = ['$label1:', 'mov $t0, $t1'];
const result = processor.getLabelFind(asmLines, mockContext);
expect(result).toBe(mockContext.labelFindMips);
});
it('should return non-MIPS regex for non-MIPS assembly', () => {
const asmLines = ['label1:', 'mov rax, rbx'];
const result = processor.getLabelFind(asmLines, mockContext);
expect(result).toBe(mockContext.labelFindNonMips);
});
});
describe('getUsedLabelsInLine', () => {
it('should extract labels from instruction line', () => {
const line = ' mov rax, label1';
const result = processor.getUsedLabelsInLine(line, mockContext);
expect(result).toHaveLength(2);
expect(result[0].name).toBe('rax');
expect(result[1].name).toBe('label1');
});
it('should handle lines with comments', () => {
const line = ' mov rax, label1 ; comment';
const result = processor.getUsedLabelsInLine(line, mockContext);
expect(result).toHaveLength(2);
expect(result[0].name).toBe('rax');
expect(result[1].name).toBe('label1');
});
it('should return empty array for label definition', () => {
const line = 'label1:';
const result = processor.getUsedLabelsInLine(line, mockContext);
expect(result).toHaveLength(0);
});
});
describe('removeLabelsWithoutDefinition', () => {
it('should remove labels without definitions', () => {
const asm: ParsedAsmResultLine[] = [
{
text: 'mov rax, label1',
source: null,
labels: [
{name: 'label1', range: {startCol: 10, endCol: 16}},
{name: 'undefined_label', range: {startCol: 18, endCol: 32}},
],
},
];
const labelDefinitions = {label1: 1};
processor.removeLabelsWithoutDefinition(asm, labelDefinitions);
expect(asm[0].labels).toHaveLength(1);
expect(asm[0].labels![0].name).toBe('label1');
});
});
describe('shouldFilterLabel', () => {
it('should filter unused labels when filters.labels is true', () => {
const match = ['label1:', 'label1', undefined] as any;
const line = 'label1:';
const labelsUsed = new Set<string>();
const result = processor.shouldFilterLabel(match, line, labelsUsed, true);
expect(result).toBe(true);
});
it('should not filter used labels', () => {
const match = ['label1:', 'label1', undefined] as any;
const line = 'label1:';
const labelsUsed = new Set(['label1']);
const result = processor.shouldFilterLabel(match, line, labelsUsed, true);
expect(result).toBe(false);
});
it('should not filter when filters.labels is false', () => {
const match = ['label1:', 'label1', undefined] as any;
const line = 'label1:';
const labelsUsed = new Set<string>();
const result = processor.shouldFilterLabel(match, line, labelsUsed, false);
expect(result).toBe(false);
});
});
});

212
test/parsing-state-tests.ts Normal file
View File

@@ -0,0 +1,212 @@
// 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 {beforeEach, describe, expect, it} from 'vitest';
import {ParsingState} from '../lib/parsers/parsing-state.js';
describe('ParsingState tests', () => {
const files = {1: '/path/to/file.cpp'};
let state: ParsingState;
beforeEach(() => {
state = new ParsingState(files, null, '', false, false, []);
});
describe('construction and initialization', () => {
it('should initialize with correct default values', () => {
expect(state.files).toBe(files);
expect(state.source).toBeNull();
expect(state.prevLabel).toBe('');
expect(state.prevLabelIsUserFunction).toBe(false);
expect(state.dontMaskFilenames).toBe(false);
expect(state.mayRemovePreviousLabel).toBe(true);
expect(state.keepInlineCode).toBe(false);
expect(state.inNvccDef).toBe(false);
expect(state.inNvccCode).toBe(false);
expect(state.inCustomAssembly).toBe(0);
expect(state.inVLIWpacket).toBe(false);
expect(state.getCurrentLineIndex()).toBe(0);
});
});
describe('source management', () => {
it('should update source and track own source', () => {
const source = {file: null, line: 42, mainsource: true};
state.updateSource(source);
expect(state.source).toBe(source);
expect(state.lastOwnSource).toBe(source);
});
it('should not update lastOwnSource for library sources', () => {
const librarySource = {file: '/usr/include/stdio.h', line: 100};
state.updateSource(librarySource);
expect(state.source).toBe(librarySource);
expect(state.lastOwnSource).toBeNull();
});
it('should reset state on block end', () => {
state.updateSource({file: null, line: 42});
state.updatePrevLabel('test_label');
state.resetToBlockEnd();
expect(state.source).toBeNull();
expect(state.prevLabel).toBe('');
expect(state.lastOwnSource).toBeNull();
});
});
describe('custom assembly handling', () => {
it('should track custom assembly nesting', () => {
expect(state.isInCustomAssembly()).toBe(false);
state.enterCustomAssembly();
expect(state.isInCustomAssembly()).toBe(true);
expect(state.inCustomAssembly).toBe(1);
state.enterCustomAssembly();
expect(state.inCustomAssembly).toBe(2);
state.exitCustomAssembly();
expect(state.inCustomAssembly).toBe(1);
expect(state.isInCustomAssembly()).toBe(true);
state.exitCustomAssembly();
expect(state.isInCustomAssembly()).toBe(false);
});
});
describe('NVCC state management', () => {
it('should handle NVCC definition state', () => {
expect(state.inNvccDef).toBe(false);
expect(state.inNvccCode).toBe(false);
state.enterNvccDef();
expect(state.inNvccDef).toBe(true);
expect(state.inNvccCode).toBe(true);
state.exitNvccDef();
expect(state.inNvccDef).toBe(false);
expect(state.inNvccCode).toBe(true);
});
});
describe('library code filtering', () => {
it('should filter library code when conditions are met', () => {
state.updatePrevLabel('lib_func', false); // not user function
state.updateSource({file: '/usr/lib/library.so', line: 100}); // library source
const result = state.shouldFilterLibraryCode({libraryCode: true});
expect(result).toBe(true);
});
it('should not filter when user function', () => {
state.updatePrevLabel('user_func', true); // user function
state.updateSource({file: '/usr/lib/library.so', line: 100});
const result = state.shouldFilterLibraryCode({libraryCode: true});
expect(result).toBe(false);
});
it('should not filter when has own source', () => {
state.updatePrevLabel('lib_func', false);
state.updateSource({file: null, line: 42, mainsource: true}); // own source
const result = state.shouldFilterLibraryCode({libraryCode: true});
expect(result).toBe(false);
});
it('should not filter when source is undefined', () => {
state.updatePrevLabel('lib_func', false); // not user function
state.updateSource(undefined); // no source
const result = state.shouldFilterLibraryCode({libraryCode: true});
expect(result).toBe(false);
});
it('should not filter when source is null', () => {
state.updatePrevLabel('lib_func', false); // not user function
state.updateSource(null); // no source
const result = state.shouldFilterLibraryCode({libraryCode: true});
expect(result).toBe(false);
});
});
describe('label management', () => {
it('should update and clear prev label', () => {
state.updatePrevLabel('test_label', true);
expect(state.prevLabel).toBe('test_label');
expect(state.prevLabelIsUserFunction).toBe(true);
state.clearPrevLabel();
expect(state.prevLabel).toBe('');
expect(state.prevLabelIsUserFunction).toBe(false);
});
});
describe('line iteration', () => {
it('should iterate through lines', () => {
const testLines = ['line1', 'line2', 'line3'];
const iterableState = new ParsingState({}, null, '', false, false, testLines);
const lines: string[] = [];
for (const line of iterableState) {
lines.push(line);
}
expect(lines).toEqual(testLines);
});
it('should track current index during iteration', () => {
const testLines = ['line1', 'line2'];
const iterableState = new ParsingState({}, null, '', false, false, testLines);
expect(iterableState.getCurrentLineIndex()).toBe(0);
const iterator = iterableState[Symbol.iterator]();
iterator.next();
expect(iterableState.getCurrentLineIndex()).toBe(1);
iterator.next();
expect(iterableState.getCurrentLineIndex()).toBe(2);
});
});
describe('flags management', () => {
it('should manage removal and inline code flags', () => {
expect(state.shouldRemovePreviousLabel()).toBe(true);
expect(state.shouldKeepInlineCode()).toBe(false);
state.setMayRemovePreviousLabel(false);
state.setKeepInlineCode(true);
expect(state.shouldRemovePreviousLabel()).toBe(false);
expect(state.shouldKeepInlineCode()).toBe(true);
});
});
});

View File

@@ -0,0 +1,171 @@
// 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 {describe, expect, it} from 'vitest';
import {SourceHandlerContext, SourceLineHandler} from '../lib/parsers/source-line-handler.js';
describe('SourceLineHandler tests', () => {
const handler = new SourceLineHandler();
const context: SourceHandlerContext = {
files: {
1: '/path/to/source.cpp',
2: '/path/to/header.h',
},
dontMaskFilenames: false,
};
describe('handleSourceTag', () => {
it('should parse basic .loc directive', () => {
const result = handler.handleSourceTag('\t.loc\t1 23 0', context);
expect(result).toEqual({
file: '/path/to/source.cpp',
line: 23,
});
});
it('should parse .loc directive with column', () => {
const result = handler.handleSourceTag('\t.loc\t1 23 5', context);
expect(result).toEqual({
file: '/path/to/source.cpp',
line: 23,
column: 5,
});
});
it('should return null for non-matching lines', () => {
const result = handler.handleSourceTag('mov rax, rbx', context);
expect(result).toBeNull();
});
it('should handle dontMaskFilenames flag', () => {
const contextWithMasking: SourceHandlerContext = {
...context,
dontMaskFilenames: true,
};
const result = handler.handleSourceTag('\t.loc\t1 23 0', contextWithMasking);
expect(result).toEqual({
file: '/path/to/source.cpp',
line: 23,
mainsource: false,
});
});
});
describe('handleD2Tag', () => {
it('should parse .d2line directive', () => {
const result = handler.handleD2Tag('\t.d2line 42');
expect(result).toEqual({
file: null,
line: 42,
});
});
it('should return null for non-matching lines', () => {
const result = handler.handleD2Tag('mov rax, rbx');
expect(result).toBeNull();
});
});
describe('handleCVTag', () => {
it('should parse .cv_loc directive', () => {
const result = handler.handleCVTag('\t.cv_loc 1 2 42 5', context);
expect(result).toEqual({
file: '/path/to/header.h',
line: 42,
column: 5,
});
});
it('should parse .cv_loc directive without column', () => {
const result = handler.handleCVTag('\t.cv_loc 1 1 23 0', context);
expect(result).toEqual({
file: '/path/to/source.cpp',
line: 23,
});
});
it('should return null for non-matching lines', () => {
const result = handler.handleCVTag('mov rax, rbx', context);
expect(result).toBeNull();
});
});
describe('handle6502Debug', () => {
it('should parse .dbg line directive', () => {
const result = handler.handle6502Debug('\t.dbg line, "test.asm", 42', context);
expect(result).toEqual({
file: 'test.asm',
line: 42,
});
});
it('should return null for .dbg line end directive', () => {
const result = handler.handle6502Debug('\t.dbg line end', context);
expect(result).toBeNull();
});
it('should return null for non-matching lines', () => {
const result = handler.handle6502Debug('mov rax, rbx', context);
expect(result).toBeNull();
});
});
describe('handleStabs', () => {
it('should handle stab type 68', () => {
const result = handler.handleStabs('\t.stabn 68,0,42,.');
expect(result).toEqual({
file: null,
line: 42,
});
});
it('should handle stab type 132', () => {
const result = handler.handleStabs('\t.stabn 132,0,42,.');
expect(result).toBeNull();
});
it('should return undefined for non-matching lines', () => {
const result = handler.handleStabs('mov rax, rbx');
expect(result).toBeUndefined();
});
});
describe('processSourceLine', () => {
it('should process source tag', () => {
const result = handler.processSourceLine('\t.loc\t1 23 0', context);
expect(result.source).toEqual({
file: '/path/to/source.cpp',
line: 23,
});
expect(result.resetPrevLabel).toBe(false);
});
it('should return undefined for non-source lines', () => {
const result = handler.processSourceLine('mov rax, rbx', context);
expect(result.source).toBeUndefined();
expect(result.resetPrevLabel).toBe(false);
});
});
});

View File

@@ -36,6 +36,13 @@ import {AsmEWAVRParser} from '../lib/parsers/asm-parser-ewavr.js';
import {SassAsmParser} from '../lib/parsers/asm-parser-sass.js';
import {VcAsmParser} from '../lib/parsers/asm-parser-vc.js';
import {AsmParser} from '../lib/parsers/asm-parser.js';
// Test helper class that extends AsmParser to allow setting protected properties for testing
class AsmParserForTest extends AsmParser {
setBinaryHideFuncReForTest(regex: RegExp | null) {
this.binaryHideFuncRe = regex;
}
}
import {CompilerProps, fakeProps} from '../lib/properties.js';
import {CompilerInfo} from '../types/compiler.interfaces.js';
import {ParseFiltersAndOutputOptions} from '../types/features/filters.interfaces.js';
@@ -112,9 +119,11 @@ export function processAsm(filename: string, filters: ParseFiltersAndOutputOptio
else if (filename.includes('cc65-')) parser = new CC65AsmParser(fakeProps({}));
else if (filename.includes('ewarm-')) parser = new AsmEWAVRParser(fakeProps({}));
else {
parser = new AsmParser();
parser.binaryHideFuncRe =
/^(__.*|_(init|start|fini)|(de)?register_tm_clones|call_gmon_start|frame_dummy|\.plt.*|_dl_relocate_static_pie)$/;
const testParser = new AsmParserForTest();
testParser.setBinaryHideFuncReForTest(
/^(__.*|_(init|start|fini)|(de)?register_tm_clones|call_gmon_start|frame_dummy|\.plt.*|_dl_relocate_static_pie)$/,
);
parser = testParser;
}
return parser.process(file, filters);
}

View File

@@ -27,11 +27,24 @@ import {beforeEach, describe, expect, it} from 'vitest';
import {VcAsmParser} from '../lib/parsers/asm-parser-vc.js';
import {AsmParser} from '../lib/parsers/asm-parser.js';
// Test helper class that extends VcAsmParser to expose protected properties for testing
class VcAsmParserForTest extends VcAsmParser {
getCommentOnlyRegexForTest() {
return this.commentOnly;
}
getDefinesFunctionRegexForTest() {
return this.definesFunction;
}
}
describe('VcAsmParser', () => {
let parser: VcAsmParser;
let testParser: VcAsmParserForTest;
beforeEach(() => {
parser = new VcAsmParser();
testParser = new VcAsmParserForTest();
});
describe('VC assembly processing functionality', () => {
@@ -79,12 +92,14 @@ describe('VcAsmParser', () => {
const indentedComment = ' ; Indented comment';
const codeLine = 'mov eax, ebx';
const commentOnlyRegex = testParser.getCommentOnlyRegexForTest();
// VC commentOnly regex is /^;/ - only matches lines starting with ;
expect(parser.commentOnly.test(commentLine)).toBe(true);
expect(parser.commentOnly.test(codeLine)).toBe(false);
expect(commentOnlyRegex.test(commentLine)).toBe(true);
expect(commentOnlyRegex.test(codeLine)).toBe(false);
// VC regex doesn't match comments with leading whitespace
expect(parser.commentOnly.test(indentedComment)).toBe(false);
expect(commentOnlyRegex.test(indentedComment)).toBe(false);
});
});
@@ -93,12 +108,14 @@ describe('VcAsmParser', () => {
const procLine = '_function\tPROC';
const nonProcLine = '_function:';
const definesFunctionRegex = testParser.getDefinesFunctionRegexForTest();
// Test the function definition regex directly
expect(parser.definesFunction.test(procLine)).toBe(true);
expect(parser.definesFunction.test(nonProcLine)).toBe(false);
expect(definesFunctionRegex.test(procLine)).toBe(true);
expect(definesFunctionRegex.test(nonProcLine)).toBe(false);
// Should extract function name
const match = procLine.match(parser.definesFunction);
const match = procLine.match(definesFunctionRegex);
expect(match?.[1]).toBe('_function');
});