// Copyright (c) 2015, 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 _ from 'underscore';

import {isString} from '../../shared/common-utils.js';
import {
    AsmResultLabel,
    AsmResultSource,
    ParsedAsmResult,
    ParsedAsmResultLine,
} from '../../types/asmresult/asmresult.interfaces.js';
import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.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';

export type ParsingContext = {
    files: Record<number, string>;
    source: AsmResultSource | undefined | null;
    dontMaskFilenames: boolean;
    prevLabel: string;
    prevLabelIsUserFunction: boolean;
};

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;
    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;

    constructor(compilerProps?: PropertyGetter) {
        super();

        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;
        this.mipsLabelDefinition = /^\$[\w$.]+:/;
        this.dataDefn =
            /^\s*\.(ascii|asciz|[1248]?byte|dc(?:\.[abdlswx])?|dcb(?:\.[bdlswx])?|ds(?:\.[bdlpswx])?|double|dword|fill|float|half|hword|int|long|octa|quad|short|single|skip|space|string(?:8|16|32|64)?|value|word|xword|zero)/;
        this.fileFind = /^\s*\.(?:cv_)?file\s+(\d+)\s+"([^"]+)"(\s+"([^"]+)")?.*/;
        // Opcode expression here matches LLVM-style opcodes of the form `%blah = opcode`
        this.hasOpcodeRe = /^\s*(%[$.A-Z_a-z][\w$.]*\s*=\s*)?[A-Za-z]/;
        this.instructionRe = /^\s*[A-Za-z]+/;
        this.identifierFindRe = /[$.@A-Z_a-z]\w*/g;
        this.hasNvccOpcodeRe = /^\s*[@A-Za-z|]/;
        this.definesFunction = /^\s*\.(type.*,\s*[#%@]function|proc\s+[.A-Z_a-z][\w$.]*:.*)$/;
        this.definesGlobal = /^\s*\.(?:globa?l|GLB|export)\s*([.A-Z_a-z][\w$.]*)/;
        this.definesWeak = /^\s*\.(?:weakext|weak)\s*([.A-Z_a-z][\w$.]*)/;
        this.indentedLabelDef = /^\s*([$.A-Z_a-z][\w$.]*):/;
        this.assignmentDef = /^\s*([$.A-Z_a-z][\w$.]*)\s*=\s*(.*)/;
        this.directive = /^\s*\..*$/;
        // These four regexes when phrased as /\s*#APP.*/ etc exhibit costly polynomial backtracking
        // Instead use ^$ and test with regex.test(line.trim()), more robust anyway
        this.startAppBlock = /^#APP.*$/;
        this.endAppBlock = /^#NO_APP.*$/;
        this.startAsmNesting = /^# Begin ASM.*$/;
        this.endAsmNesting = /^# End ASM.*$/;
        this.cudaBeginDef = /\.(entry|func)\s+(?:\([^)]*\)\s*)?([$.A-Z_a-z][\w$.]*)\($/;
        this.cudaEndDef = /^\s*\)\s*$/;

        this.binaryHideFuncRe = null;
        this.maxAsmLines = 5000;
        if (compilerProps) {
            const binaryHideFuncReValue = compilerProps('binaryHideFuncRe');
            if (binaryHideFuncReValue) {
                assert(isString(binaryHideFuncReValue));
                this.binaryHideFuncRe = new RegExp(binaryHideFuncReValue);
            }

            this.maxAsmLines = compilerProps('maxLinesOfAsm', this.maxAsmLines);
        }

        this.asmOpcodeRe = /^\s*(?<address>[\da-f]+):\s*(?<opcodes>([\da-f]{2} ?)+)\s*(?<disasm>.*)/;
        this.relocationRe = /^\s*(?<address>[\da-f]+):\s*(?<relocname>(R_[\dA-Z_]+))\s*(?<relocdata>.*)/;
        this.relocDataSymNameRe = /^(?<symname>[^\d-+][\w.]*)?\s*(?<addend_or_value>.*)$/;
        if (process.platform === 'win32') {
            this.lineRe = /^([A-Z]:\/[^:]+):(?<line>\d+).*/;
        } else {
            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 <>).
        this.labelRe = /^([\da-f]+)\s+<(.+)>:$/;
        this.destRe = /\s([\da-f]+)\s+<([^+>]+)(\+0x[\da-f]+)?>$/;
        this.commentRe = /[#;]/;
        this.instOpcodeRe = /(\.inst\.?\w?)\s*(.*)/;

        // Lines matching the following pattern are considered comments:
        // - starts with '#', '@', '//' or a single ';' (non repeated)
        // - starts with ';;' and the first non-whitespace before end of line is not #
        this.commentOnly = /^\s*(((#|@|\/\/).*)|(\/\*.*\*\/)|(;\s*)|(;[^;].*)|(;;\s*[^\s#].*))$/;
        this.commentOnlyNvcc = /^\s*(((#|;|\/\/).*)|(\/\*.*\*\/))$/;
        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>/;
        this.endBlock = /\.(cfi_endproc|data|text|section)/;
        this.blockComments = /^[\t ]*\/\*(\*(?!\/)|[^*])*\*\/\s*/gm;
    }

    checkVLIWpacket(line: string, inVLIWpacket: boolean) {
        return inVLIWpacket;
    }

    hasOpcode(line: string, inNvccCode: boolean = false, inVLIWpacket: boolean = false) {
        // Remove any leading label definition...
        const match = line.match(this.labelDef);
        if (match) {
            line = line.substring(match[0].length);
        }
        // Strip any comments
        line = line.split(this.commentRe, 1)[0];
        // .inst generates an opcode, so also counts
        if (this.instOpcodeRe.test(line)) return true;
        // Detect assignment, that's not an opcode...
        if (this.assignmentDef.test(line)) return false;
        if (inNvccCode) {
            return !!this.hasNvccOpcodeRe.test(line);
        }
        return !!this.hasOpcodeRe.test(line);
    }

    labelFindFor(asmLines: string[]) {
        const isMips = _.any(asmLines, line => !!this.mipsLabelDefinition.test(line));
        return isMips ? this.labelFindMips : this.labelFindNonMips;
    }

    findUsedLabels(asmLines: string[], filterDirectives?: boolean) {
        const labelsUsed: Record<string, boolean> = {};
        const weakUsages: Record<string, Array<string>> = {};
        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;

        // 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;
            } else {
                inLabelGroup = false;
            }
            match = line.match(this.definesGlobal);
            if (!match) match = line.match(this.definesWeak);
            if (!match) match = line.match(this.cudaBeginDef);
            if (match) {
                labelsUsed[match[1]] = true;
            }

            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[label] = true;
            } 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) {
                    for (const currentLabel of currentLabelSet) {
                        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[label] = true;
                        } else {
                            if (!weakUsages[currentLabel]) weakUsages[currentLabel] = [];
                            for (const label of match) weakUsages[currentLabel].push(label);
                        }
                    }
                }
            }
        }

        // Now follow the chains of used labels, marking any weak references they refer
        // to as also used. We iteratively do this until either no new labels are found,
        // or we hit a limit (only here to prevent a pathological case from hanging).
        function markUsed(label: string) {
            labelsUsed[label] = true;
        }

        const MaxLabelIterations = 10;
        for (let iter = 0; iter < MaxLabelIterations; ++iter) {
            const toAdd: string[] = [];
            _.each(labelsUsed, (t, label) => {
                // jshint ignore:line
                _.each(weakUsages[label], (nowused: string) => {
                    if (labelsUsed[nowused]) return;
                    toAdd.push(nowused);
                });
            });
            if (!toAdd) break;
            _.each(toAdd, markUsed);
        }
        return labelsUsed;
    }

    parseFiles(asmLines: string[]) {
        const files: Record<number, string> = {};
        for (const line of asmLines) {
            const match = line.match(this.fileFind);
            if (match) {
                const lineNum = 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];
                    }
                } 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.name]);
            }
        }
    }

    // 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, (label, index) => {
            const startCol = removedCol + index;
            labelsInLine.push({
                name: label,
                range: {
                    startCol: startCol,
                    endCol: startCol + label.length,
                },
            });
            return label;
        });

        return labelsInLine;
    }

    protected isUserFunctionByLookingAhead(context: ParsingContext, asmLines: string[], idxFrom: number): boolean {
        const funcContext: ParsingContext = {
            files: context.files,
            source: undefined,
            dontMaskFilenames: true,
            prevLabelIsUserFunction: false,
            prevLabel: '',
        };

        for (let idx = idxFrom; idx < asmLines.length; idx++) {
            const line = asmLines[idx];

            const endprocMatch = line.match(this.endBlock);
            if (endprocMatch) return false;

            this.handleSource(funcContext, line);
            this.handleStabs(funcContext, line);
            this.handle6502(funcContext, line);

            if (funcContext.source?.mainsource) return true;
        }

        return false;
    }

    protected handleSource(context: ParsingContext, line: string) {
        let match = line.match(this.sourceTag);
        if (match) {
            const file = utils.maskRootdir(context.files[parseInt(match[1])]);
            const sourceLine = 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 = parseInt(match[3]);
                if (!isNaN(sourceCol) && sourceCol !== 0) {
                    context.source.column = sourceCol;
                }
            } else {
                context.source = null;
            }
        } else {
            match = line.match(this.sourceD2Tag);
            if (match) {
                const sourceLine = 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 = parseInt(match[3]);
                    const file = utils.maskRootdir(context.files[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 = parseInt(match[4]);
                    if (!isNaN(sourceCol) && sourceCol !== 0) {
                        context.source.column = sourceCol;
                    }
                }
            }
        }
    }

    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 (parseInt(match[1])) {
            case 68: {
                context.source = {file: null, line: parseInt(match[2])};
                break;
            }
            case 132:
            case 100: {
                context.source = null;
                context.prevLabel = '';
                break;
            }
        }
    }

    protected handle6502(context: ParsingContext, line: string) {
        const match = line.match(this.source6502Dbg);
        if (match) {
            const file = utils.maskRootdir(match[1]);
            const sourceLine = 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;
        }
    }

    processAsm(asmResult: string, filters: ParseFiltersAndOutputOptions): ParsedAsmResult {
        if (filters.binary || filters.binaryObject) return this.processBinaryAsm(asmResult, filters);

        const startTime = process.hrtime.bigint();

        if (filters.commentOnly) {
            // Remove any block comments that start and end on a line if we're removing comment-only lines.
            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);
        }

        const labelsUsed = this.findUsedLabels(asmLines, filters.directives);

        let mayRemovePreviousLabel = true;
        let keepInlineCode = false;

        let lastOwnSource: AsmResultSource | undefined | null;

        const context: ParsingContext = {
            files: this.parseFiles(asmLines),
            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
        // eslint-disable-next-line max-statements
        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[match[1]] === undefined &&
                    match[0] === line &&
                    (match[2] === undefined || match[2].trim() === '.')
                ) {
                    // It's an unused label.
                    if (filters.labels) {
                        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
                    if (this.directive.test(line) && !this.instOpcodeRe.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,
            });
        }

        this.removeLabelsWithoutDefinition(asm, labelDefinitions);

        const endTime = process.hrtime.bigint();
        return {
            asm: asm,
            labelDefinitions: labelDefinitions,
            parsingTime: utils.deltaTimeNanoToMili(startTime, endTime),
            filteredCount: startingLineCount - asm.length,
        };
    }

    fixLabelIndentation(line: string) {
        const match = line.match(this.indentedLabelDef);
        if (match) {
            return line.replace(/^\s+/, '');
        } else {
            return line;
        }
    }

    isUserFunction(func: string) {
        if (this.binaryHideFuncRe === null) return true;

        return !this.binaryHideFuncRe.test(func);
    }

    processBinaryAsm(asmResult: string, filters: ParseFiltersAndOutputOptions): ParsedAsmResult {
        const startTime = process.hrtime.bigint();
        const asm: ParsedAsmResultLine[] = [];
        const labelDefinitions: Record<string, number> = {};
        const dontMaskFilenames = filters.dontMaskFilenames;

        let asmLines = utils.splitLines(asmResult);
        const startingLineCount = asmLines.length;
        let source: AsmResultSource | undefined | null = null;
        let func: string | null = null;
        let mayRemovePreviousLabel = true;

        // Handle "error" documents.
        if (asmLines.length === 1 && asmLines[0][0] === '<') {
            return {
                asm: [{text: asmLines[0], source: null}],
            };
        }

        if (filters.preProcessBinaryAsmLines !== undefined) {
            asmLines = filters.preProcessBinaryAsmLines(asmLines);
        }

        for (const line of asmLines) {
            const labelsInLine: AsmResultLabel[] = [];

            if (asm.length >= this.maxAsmLines) {
                if (asm.length === this.maxAsmLines) {
                    asm.push({
                        text: '[truncated; too many lines]',
                        source: null,
                        labels: labelsInLine,
                    });
                }
                continue;
            }
            let match = line.match(this.lineRe);
            if (match) {
                assert(match.groups);
                if (dontMaskFilenames) {
                    source = {
                        file: utils.maskRootdir(match[1]),
                        line: parseInt(match.groups.line),
                        mainsource: true,
                    };
                } else {
                    source = {file: null, line: parseInt(match.groups.line), mainsource: true};
                }
                continue;
            }

            match = line.match(this.labelRe);
            if (match) {
                func = match[2];
                if (func && this.isUserFunction(func)) {
                    asm.push({
                        text: func + ':',
                        source: null,
                        labels: labelsInLine,
                    });
                    labelDefinitions[func] = asm.length;
                    if (process.platform === 'win32') source = null;
                }
                continue;
            }

            if (func && line === `${func}():`) continue;

            if (!func || !this.isUserFunction(func)) continue;

            // note: normally the source.file will be null if it's code from example.ext
            //  but with filters.dontMaskFilenames it will be filled with the actual filename
            //  instead we can test source.mainsource in that situation
            const isMainsource = source && (source.file === null || source.mainsource);
            if (filters.libraryCode && !isMainsource) {
                if (mayRemovePreviousLabel && asm.length > 0) {
                    const lastLine = asm[asm.length - 1];
                    if (lastLine.text && this.labelDef.test(lastLine.text)) {
                        asm.pop();
                    }
                    mayRemovePreviousLabel = false;
                }
                continue;
            } else {
                mayRemovePreviousLabel = true;
            }

            match = line.match(this.asmOpcodeRe);
            if (match) {
                assert(match.groups);
                const address = parseInt(match.groups.address, 16);
                const opcodes = (match.groups.opcodes || '').split(' ').filter(x => !!x);
                const disassembly = ' ' + AsmRegex.filterAsmLine(match.groups.disasm, filters);
                const destMatch = line.match(this.destRe);
                if (destMatch) {
                    const labelName = destMatch[2];
                    const startCol = disassembly.indexOf(labelName) + 1;
                    labelsInLine.push({
                        name: labelName,
                        range: {
                            startCol: startCol,
                            endCol: startCol + labelName.length,
                        },
                    });
                }
                asm.push({
                    opcodes: opcodes,
                    address: address,
                    text: disassembly,
                    source: source,
                    labels: labelsInLine,
                });
            }

            match = line.match(this.relocationRe);
            if (match) {
                assert(match.groups);
                const address = parseInt(match.groups.address, 16);
                const relocname = match.groups.relocname;
                const relocdata = match.groups.relocdata;
                // value/addend matched but not used yet.
                // const match_value = relocdata.match(this.relocDataSymNameRe);
                asm.push({
                    text: `   ${relocname} ${relocdata}`,
                    address: address,
                });
            }
        }

        this.removeLabelsWithoutDefinition(asm, labelDefinitions);

        const endTime = process.hrtime.bigint();

        return {
            asm: asm,
            labelDefinitions: labelDefinitions,
            parsingTime: utils.deltaTimeNanoToMili(startTime, endTime),
            filteredCount: startingLineCount - asm.length,
        };
    }

    process(asm: string, filters: ParseFiltersAndOutputOptions) {
        return this.processAsm(asm, filters);
    }
}
