// Copyright (c) 2018, Microsoft Corporation // 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 {ParsedAsmResult, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces.js'; import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js'; import {assert} from '../assert.js'; import {logger} from '../logger.js'; import {PropertyGetter} from '../properties.interfaces.js'; import * as utils from '../utils.js'; import {AsmParser} from './asm-parser.js'; import {AsmRegex} from './asmregex.js'; type Source = {file: string | null; line: number}; type Line = {text: string; source: Source | null}; type Label = { lines: Line[]; name: string | undefined; initialLine: number | undefined; file: string | null | undefined; }; type ResultObject = { prefix: Line[]; functions: Label[]; postfix: Line | null; }; export class VcAsmParser extends AsmParser { private readonly asmBinaryParser: AsmParser; private readonly filenameComment = /^; File (.+)/; protected miscDirective = /^\s*(include|INCLUDELIB|TITLE|\.|THUMB|ARM64|TTL|END$)/; private readonly localLabelDef = /^([$A-Z_a-z]+) =/; private readonly lineNumberComment = /^; Line (\d+)/; protected beginSegment = /^(CONST|_BSS|\.?[prx]?data(\$[A-Za-z]+)?|CRT(\$[A-Za-z]+)?|_TEXT|\.?text(\$[A-Za-z]+)?)\s+SEGMENT|\s*AREA/; protected endSegment = /^(CONST|_BSS|[prx]?data(\$[A-Za-z]+)?|CRT(\$[A-Za-z]+)?|_TEXT|text(\$[A-Za-z]+)?)\s+ENDS/; private readonly beginFunction = /^; Function compile flags: /; private readonly endProc = /^([$?@A-Z_a-z][\w$<>?@]*)?\s+ENDP/; private readonly labelFind = /[$?@A-Z_a-z][\w$<>?@]*/g; constructor(compilerProps?: PropertyGetter) { super(compilerProps); this.asmBinaryParser = new AsmParser(compilerProps); this.commentOnly = /^;/; this.labelDef = /^\|?([$?@A-Z_a-z][\w$<>?@]*)\|?\s+(PROC|=|D[BDQW])/; this.definesGlobal = /^\s*(PUBLIC|EXTRN|EXPORT)\s+/; this.definesFunction = /^\|?([$?@A-Z_a-z][\w$<>?@]*)\|?\s+PROC/; this.dataDefn = /^(\|?[$?@A-Z_a-z][\w$<>?@]*\|?)\sDC?[BDQW]\s|\s+DC?[BDQW]\s|\s+ORG/; // these are set to an impossible regex, because VC doesn't have inline assembly this.startAppBlock = this.startAsmNesting = /a^/; this.endAppBlock = this.endAsmNesting = /a^/; // same, but for CUDA this.cudaBeginDef = /a^/; } override hasOpcode(line: string) { // note: cl doesn't output leading labels // strip comments line = line.split(';', 1)[0]; // check for empty lines if (line.length === 0) return false; // check for a local label definition if (this.localLabelDef.test(line)) return false; // check for global label definitions if (this.definesGlobal.test(line)) return false; // check for data definitions if (this.dataDefn.test(line)) return false; // check for segment begin and end if (this.beginSegment.test(line) || this.endSegment.test(line)) return false; // check for function begin and end // note: functionBegin is used for the function compile flags comment if (this.definesFunction.test(line) || this.endProc.test(line)) return false; // check for miscellaneous directives if (this.miscDirective.test(line)) return false; return this.hasOpcodeRe.test(line); } override labelFindFor() { return this.labelFind; } override processAsm(asm: string, filters: ParseFiltersAndOutputOptions): ParsedAsmResult { if (filters.binary || filters.binaryObject) { return this.asmBinaryParser.processAsm(asm, filters); } const getFilenameFromComment = line => { const matches = line.match(this.filenameComment); if (matches) { return matches[1]; } else { return null; } }; const getLineNumberFromComment = line => { const matches = line.match(this.lineNumberComment); if (matches) { return parseInt(matches[1]); } else { return null; } }; const asmLines = utils.splitLines(asm); // note: VC doesn't output unused labels, afaict const stdInLooking = /|^-$|example\.[^/]+$|/; const resultObject: ResultObject = { prefix: [], functions: [], postfix: null, }; let currentFunction: Label | null = null; let currentFile: string | undefined | null; let currentLine: number | undefined; let seenEnd = false; const datadefLabels: string[] = []; const datadefLabelsUsed: string[] = []; const createSourceFor = (hasopc, currentFile, currentLine) => { if (hasopc && (currentFile || currentLine)) { return { file: currentFile || null, line: currentLine || null, }; } return null; }; const checkUsedDatadefLabels = line => { const labels = line.match(this.labelFind); if (!labels) return; labels.splice(0, 1); for (const item of labels) { if (datadefLabels.find(l => item === l)) { datadefLabelsUsed.push(item); } } }; const checkBeginFunction = line => { if (this.beginFunction.test(line)) { currentFunction = { lines: [], initialLine: undefined, name: undefined, file: undefined, }; resultObject.functions.push(currentFunction); } return currentFunction; }; const checkForDdefLabel = line => { const ddef = line.match(this.dataDefn); if (ddef && ddef[1]) { datadefLabels.push(ddef[1]); } }; for (let line of asmLines) { if (line.trim() === 'END') { seenEnd = true; if (!filters.directives) { resultObject.postfix = {text: line, source: null}; } continue; } if (line.trim() === '') { if (seenEnd) continue; const emptyLine = {text: '', source: null}; if (currentFunction === null) { resultObject.prefix.push(emptyLine); } else { currentFunction.lines.push(emptyLine); } continue; } if (seenEnd) { // this should never happen throw new Error('Visual C++: text after the end statement'); } let tmp = getFilenameFromComment(line); if (tmp === null) { tmp = getLineNumberFromComment(line); if (tmp !== null) { if (currentFile === undefined) { logger.error('Somehow, we have a line number comment without a file comment: %s', line); } assert(currentFunction); if (currentFunction.initialLine === undefined) { currentFunction.initialLine = tmp; } currentLine = tmp; } } else { if (currentFunction === null) { logger.error('We have a file comment outside of a function: %s', line); } // if the file is the "main file", give it the file `null` if (stdInLooking.test(tmp)) { currentFile = null; } else { currentFile = tmp; } assert(currentFunction); if (currentFunction.file === undefined) { currentFunction.file = currentFile; } } currentFunction = checkBeginFunction(line); const functionName = line.match(this.definesFunction); if (functionName) { if (asmLines.length === 0) { continue; } assert(currentFunction); currentFunction.name = functionName[1]; } if (filters.commentOnly && this.commentOnly.test(line)) continue; const shouldSkip = filters.directives && (line.match(this.endSegment) || line.match(this.definesGlobal) || line.match(this.miscDirective) || line.match(this.beginSegment)); if (shouldSkip) { continue; } checkForDdefLabel(line); line = utils.expandTabs(line); const hasopc = this.hasOpcode(line); const textAndSource = { text: AsmRegex.filterAsmLine(line, filters), source: createSourceFor(hasopc, currentFile, currentLine), }; if (currentFunction === null) { resultObject.prefix.push(textAndSource); } else if (!shouldSkip) { currentFunction.lines.push(textAndSource); } checkUsedDatadefLabels(line); } return this.resultObjectIntoArray(resultObject, filters, datadefLabelsUsed); } resultObjectIntoArray( obj: ResultObject, filters: ParseFiltersAndOutputOptions, ddefLabelsUsed: string[], ): ParsedAsmResult { const collator = new Intl.Collator(); obj.functions.sort((f1, f2) => { // order the main file above all others if (f1.file === null && f2.file !== null) { return -1; } if (f1.file !== null && f2.file === null) { return 1; } // order no-file below all others if (f1.file === undefined && f2.file !== undefined) { return 1; } if (f1.file !== undefined && f2.file === undefined) { return -1; } // if the files are the same, use line number ordering if (f1.file === f2.file) { // if the lines are the same as well, it's either: // - two template instantiations, or // - two compiler generated functions // order by name if (f1.initialLine === f2.initialLine) { return collator.compare(f1.name || '', f2.name || ''); } else { // NOTE: initialLine can be undefined here, that's ok return (f1.initialLine as number) - (f2.initialLine as number); } } // else, order by file assert(typeof f1.file === 'string' && typeof f2.file === 'string'); return collator.compare(f1.file, f2.file); }); const result: ParsedAsmResultLine[] = []; let lastLineWasWhitespace = true; const pushLine = line => { if (line.text.trim() === '') { if (!lastLineWasWhitespace) { result.push({text: '', source: null}); lastLineWasWhitespace = true; } } else { result.push(line); lastLineWasWhitespace = false; } }; if (filters.labels) { let currentDdef: string | undefined; let isUsed = false; for (const line of obj.prefix) { const matches = line.text.match(this.dataDefn); if (matches) { if (matches[1]) { currentDdef = matches[1]; isUsed = !!ddefLabelsUsed.find(label => currentDdef === label); } if (isUsed) { pushLine(line); } } else { currentDdef = undefined; pushLine(line); } } } else { for (const line of obj.prefix) { pushLine(line); } } for (const func of obj.functions) { if (!filters.libraryCode || func.file === null) { pushLine({text: '', source: null}); for (const line of func.lines) { pushLine(line); } } } if (obj.postfix !== null) { pushLine(obj.postfix); } return { asm: result, }; } }