| // Copyright (c) 2019, Ethan Slattery |
| // 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'; |
| import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces'; |
| import {assert} from '../assert'; |
| import {logger} from '../logger'; |
| import {PropertyGetter} from '../properties.interfaces'; |
| import * as utils from '../utils'; |
| |
| import {AsmParser} from './asm-parser'; |
| import {AsmRegex} from './asmregex'; |
| |
| // this file uses null throughout for "not there" not undefined. |
| type Source = {file: string | null; line: number}; |
| type Line = {text: string; source: Source | null}; |
| type Label = { |
| lines: Line[]; |
| name: string | null; |
| initialLine: number; |
| file: string | null; |
| require: string[]; |
| }; |
| type ResultObject = { |
| prefix: Line[]; |
| labels: Label[]; |
| postfix: Line[]; |
| }; |
| |
| export class AsmEWAVRParser extends AsmParser { |
| private readonly filenameComment = /^\/\/\s[A-Za-z]?:?\S(([^/\\]*[/\\])+)([^/\\]+)$/; |
| private readonly lineNumberComment = /^\/\/\s*(\d+)\s(?!bytes).*/; |
| |
| // categories of directives. remove if filters.directives set |
| private readonly segmentBegin = /^\s*(ASEG|ASEGN|COMMON|RSEG|STACK|SECTION)\s*(.[\w():]*)/; |
| private readonly segmentControl = /^\s*(ALIGN|EVEN|ODD|ORG|SECTION_GROUP|SECTION_TYPE|DATA)/; |
| private readonly definesLocal = /^\s*((ASSIGN|DEFINE|LOCAL|ALIAS|EQU|VAR)|(([A-Z_a-z]\w*)=.+))$/; |
| private readonly miscDirective = /^\s*(NAME|MODULE|PROGRAM|LIBRARY|ERROR|END|CASEOFF|CASEON|CFI|COL|RADIX|THUMB)/; |
| private readonly dataStatement = /^\s*(DB|DC16|DC24|DC32|DC8|DD|DP|DS|DS16|DS24|DS32|DW)/; |
| private readonly requireStatement = /^\s*REQUIRE\s+`?\?*<?([A-Z_a-z][\w ]*)>?`?/; |
| |
| constructor(compilerProps: PropertyGetter) { |
| super(compilerProps); |
| this.commentOnly = /^\s*(((#|@|\$|\/\/).*)|(\/\*.*\*\/))$/; |
| this.definesGlobal = /^\s*(EXTERN|EXTRN|IMPORT|EXPORT|PUBWEAK|PUBLIC)\s+(.+)$/; |
| // NOTE: Compiler generated labels can have spaces in them, but are quoted and in <> |
| this.labelDef = /^`?\?*<?([A-Z_a-z][\w :]*)>?`?:$/; |
| } |
| |
| override hasOpcode(line: string): boolean { |
| // check for empty lines |
| if (line.length === 0) return false; |
| // check for a local label definition |
| if (this.definesLocal.test(line)) return false; |
| // check for global label definitions |
| if (this.definesGlobal.test(line)) return false; |
| // check for data definitions |
| if (this.dataStatement.test(line)) return false; |
| // check for segment begin and end |
| if (this.segmentBegin.test(line) || this.segmentControl.test(line)) return false; |
| // check for label definition |
| if (this.labelDef.test(line)) return false; |
| // check for miscellaneous directives |
| if (this.miscDirective.test(line)) return false; |
| // check for requre statement |
| if (this.requireStatement.test(line)) return false; |
| |
| return this.hasOpcodeRe.test(line); |
| } |
| |
| override labelFindFor() { |
| return this.labelDef; |
| } |
| |
| override processAsm(asm: string, filters: ParseFiltersAndOutputOptions): ParsedAsmResult { |
| // NOTE: EWAVR assembly seems to be closest to visual studio |
| const getFilenameFromComment = line => { |
| const matches = line.match(this.filenameComment); |
| if (matches) { |
| return matches[3]; |
| } 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); |
| |
| const stdInLooking = /<stdin>|^-$|example\.[^/]+$|<source>/; |
| |
| const resultObject: ResultObject = { |
| prefix: [], |
| labels: [], |
| postfix: [], |
| }; |
| |
| let currentLabel: Label | null = null; |
| let currentFile: string | undefined | null; |
| let currentLine: number | undefined; |
| |
| let seenEnd = false; |
| |
| const definedLabels: Record<string, number> = {}; |
| |
| const createSourceFor = (line, currentFile, currentLine) => { |
| const hasopc = this.hasOpcode(line); |
| const createsData = line.match(this.dataStatement); |
| if ((hasopc || createsData) && (currentFile || currentLine)) { |
| return { |
| file: currentFile || null, |
| line: currentLine || null, |
| }; |
| } |
| |
| return null; |
| }; |
| |
| const checkBeginLabel = line => { |
| const matches = line.match(this.labelDef); |
| if (matches && currentLine) { |
| currentLabel = { |
| lines: [], |
| initialLine: currentLine, |
| name: matches[1], |
| file: currentFile || null, |
| require: [], |
| }; |
| definedLabels[matches[1]] = currentLine; |
| resultObject.labels.push(currentLabel); |
| } |
| return currentLabel; |
| }; |
| |
| const checkRequiresStatement = line => { |
| const matches = line.match(this.requireStatement); |
| if (matches && currentLabel != null) { |
| if (currentLabel.require == null) { |
| currentLabel.require = [matches[1]]; |
| } else { |
| currentLabel.require.push(matches[1]); |
| } |
| } |
| }; |
| |
| for (let line of asmLines) { |
| if (line.trim() === 'END') { |
| seenEnd = true; |
| if (!filters.directives) { |
| resultObject.postfix.push({text: line, source: null}); |
| } |
| continue; |
| } |
| |
| if (line.trim() === '') { |
| const emptyLine = {text: '', source: null}; |
| if (seenEnd) { |
| resultObject.postfix.push(emptyLine); |
| } else if (currentLabel === null) { |
| resultObject.prefix.push(emptyLine); |
| } else { |
| currentLabel.lines.push(emptyLine); |
| } |
| continue; |
| } |
| if (seenEnd && !this.commentOnly.test(line)) { |
| // There should be nothing but comments after END directive |
| throw new Error('EWAVR: non-comment line 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); |
| } |
| if (currentLabel !== null && currentLabel.initialLine === undefined) { |
| currentLabel.initialLine = tmp; |
| } |
| currentLine = tmp; |
| } |
| } else { |
| // if the file is the "main file", give it the file `null` |
| if (stdInLooking.test(tmp)) { |
| currentFile = null; |
| } else { |
| currentFile = tmp; |
| } |
| if (currentLabel != null && currentLabel.file === undefined) { |
| currentLabel.file = currentFile || null; |
| } |
| } |
| |
| currentLabel = checkBeginLabel(line); |
| checkRequiresStatement(line); |
| |
| if (filters.commentOnly && this.commentOnly.test(line)) { |
| continue; |
| } |
| |
| const shouldSkip = |
| filters.directives && |
| (line.match(this.segmentBegin) || |
| line.match(this.segmentControl) || |
| line.match(this.definesGlobal) || |
| line.match(this.definesLocal) || |
| line.match(this.miscDirective) || |
| line.match(this.requireStatement)); |
| |
| if (shouldSkip) { |
| continue; |
| } |
| |
| line = utils.expandTabs(line); |
| const textAndSource = { |
| text: AsmRegex.filterAsmLine(line, filters), |
| source: createSourceFor(line, currentFile, currentLine), |
| }; |
| if (currentLabel === null) { |
| resultObject.prefix.push(textAndSource); |
| } else if (!shouldSkip) { |
| currentLabel.lines.push(textAndSource); |
| } |
| } |
| |
| return this.resultObjectIntoArray(resultObject, filters, definedLabels); |
| } |
| |
| resultObjectIntoArray( |
| obj: ResultObject, |
| filters: ParseFiltersAndOutputOptions, |
| ddefLabels: Record<string, number>, |
| ): ParsedAsmResult { |
| // NOTES on EWAVR function and labels: |
| // - template functions are not mangled with type info. |
| // Instead they simply have a `_#` appended to the end, with # |
| // incrementing for each instantiation. |
| // - labels for variables, functions, and code fragments are all the same. |
| // - One exception.. functions SEEM to always have a segment command |
| // with a few lines before the label. is this reliable? |
| |
| // NOTES: compiler generated labels |
| // 'Initializer for' is used to init variables. usually at end of file |
| // 'Segment init:' is used to init sections. One per many 'Initializer for' labels. |
| const compilerGeneratedLabel = /^(initializer for |segment init: )([\w :]*)$/i; |
| const segInitLabel = /^segment init: ([\w :]*)$/i; |
| |
| 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; |
| } |
| }; |
| |
| for (const line of obj.prefix) { |
| pushLine(line); |
| } |
| |
| for (const label of obj.labels) { |
| if (!filters.libraryCode || label.file === null) { |
| assert(label.name); |
| const match = label.name.match(compilerGeneratedLabel); |
| const segInitMatch = label.name.match(segInitLabel); |
| pushLine({text: '', source: null}); |
| for (const line of label.lines) { |
| // Match variable inits to the source line of declaration. |
| // No source line for global section initilization |
| if (match && line.source != null) { |
| line.source.line = ddefLabels[match[2]]; |
| } |
| // Filter section inits as directives |
| if (segInitMatch && filters.directives) { |
| continue; |
| } |
| pushLine(line); |
| } |
| } |
| } |
| |
| for (const line of obj.postfix) { |
| pushLine(line); |
| } |
| |
| return { |
| asm: result, |
| }; |
| } |
| } |