import assert from "assert"; import * as fs from "fs"; import { WASIRunner } from "../../src/common/wasi/wasishim"; import { CodeListingMap, WorkerError } from "../../src/common/workertypes"; import { dedupeErrors, expandTabs, parseDASMListing, parseDASMListingLine, parseDASMOutput, parseSymbolMap } from "../../src/worker/tools/dasm"; import { getListingForFile, processListings } from "../../src/common/projectcore"; // Assemble sources with the same DASM build the worker uses, and hand the // listing back so the parser is always tested against real DASM output. interface DASMRun { errno: number; stdout: string; listing: string; symbols: string; output: Uint8Array; } function runDASM(files: { [path: string]: string }, mainfile: string, args: string[] = []): DASMRun { const wasi = new WASIRunner(); wasi.initSync(new WebAssembly.Module(fs.readFileSync('./src/worker/wasm/dasm-wasisdk.wasm'))); for (const path in files) { wasi.fs.putFile("./" + path, files[path]); } wasi.addPreopenDirectory("."); wasi.setArgs(['dasm', mainfile, '-f3', '-la.lst', '-sa.sym', '-oa.bin', ...args]); let errno = 0; try { errno = wasi.run(); } catch (e) { errno = -1; } function read(path: string) { try { return wasi.fs.getFile("./" + path).getBytesAsString(); } catch (e) { return null; } } let output: Uint8Array = null; try { output = wasi.fs.getFile("./a.bin").getBytes(); } catch (e) { } return { errno, stdout: wasi.fds[1].getBytesAsString(), listing: read('a.lst'), symbols: read('a.sym'), output }; } // parse a run the way the worker's assembleDASM() does function parseRun(run: DASMRun, paths: string[]) { const errors: WorkerError[] = []; const unresolved = {}; const fatal = parseDASMOutput(run.stdout, errors, unresolved); const listings: CodeListingMap = {}; for (const path of paths) listings[path] = { lines: [] }; parseDASMListing('a.lst', run.listing, listings, errors, unresolved); return { errors: dedupeErrors(errors), listings, unresolved, fatal }; } const MACRO_SRC = '\tprocessor 6502\n' + '\torg $f000\n' + ' MAC mack\n' + ' lda #0\n' + ' ENDM\n' + 'foo: mack\n' + ' mack\n'; describe('DASM listing parser', function () { it('expands tabs to 8-column stops', function () { assert.strictEqual(expandTabs('a\tb'), 'a b'); assert.strictEqual(expandTabs('12345678\tx'), '12345678 x'); assert.strictEqual(expandTabs('\t\t|'), ' |'); }); it('splits a listing line into its columns', function () { const l = parseDASMListingLine(' 10 f009\t\t d0 fb\t\t bne\t.CLEAR_STACK'); assert.strictEqual(l.linenum, 10); assert.strictEqual(l.offset, 0xf009); assert.strictEqual(l.insns, 'd0 fb'); assert.strictEqual(l.op.split(/\s+/)[0], 'bne'); }); it('ignores the bytes of unresolved and uninitialized lines', function () { // an equate: address is ???? even though a value is shown const equ = parseDASMListingLine(' 4 0000 ????\t 00 6a\t VERSION_MACRO =\t106'); assert.strictEqual(equ.insns, null); // a ds in an uninitialized segment const ds = parseDASMListingLine(' 146 U0006\t\t 00\t CXBLPF ds\t1\t; $06'); assert.strictEqual(ds.insns, null); // an equate outside any segment gets the 5-digit address 10000, which // pushes "????" over a column but leaves the later columns alone const abs = parseDASMListingLine(' 4 10000 ????\t\t00 69\t VERSION_VCS =\t105'); assert.strictEqual(abs.insns, null); assert.strictEqual(abs.offset, 0x10000); assert.strictEqual(abs.op, '= 105'); }); it('separates a label that overflows its column', function () { const l = parseDASMListingLine(' 3 f000\t\t a9 00\t AVeryLongLabelName123456 lda\t#0'); assert.strictEqual(l.insns, 'a9 00'); assert.strictEqual(l.op, 'lda #0'); // a byte field that overflows keeps only the bytes, not the '*' marker const many = parseDASMListingLine(' 5 f002\t\t 01 02 03 04*\t dc.b\t1,2,3'); assert.strictEqual(many.insns, '01 02 03 04'); }); it('is not confused by non-listing lines', function () { assert.strictEqual(parseDASMListingLine('------- FILE src.dasm LEVEL 1 PASS 1'), null); assert.strictEqual(parseDASMListingLine("src.dasm (3): error: Unknown Mnemonic 'asl a'."), null); assert.strictEqual(parseDASMListingLine(''), null); }); it('maps macro expansions back to the macro definition', function () { const run = runDASM({ 'src.dasm': MACRO_SRC }, 'src.dasm'); assert.strictEqual(run.errno, 0, run.stdout); const { errors, listings } = parseRun(run, ['src.dasm']); assert.deepStrictEqual(errors, []); const lines = listings['src.dasm'].lines; // two invocations (source lines 6 and 7), each expanding "lda #0" on line 4 assert.deepStrictEqual(lines.map((l) => l.line), [6, 4, 7, 4]); assert.deepStrictEqual(lines.map((l) => l.offset), [0xf000, 0xf000, 0xf002, 0xf002]); assert.deepStrictEqual(lines.map((l) => l.insns), [null, 'a9 00', null, 'a9 00']); }); it('tracks include files and their line numbers', function () { const files = { 'main.dasm': '\tprocessor 6502\n\torg $f000\n\tinclude "sub.asm"\nfoo:\tlda #2\n', 'sub.asm': '\tlda #1\n\tnop\n', }; const run = runDASM(files, 'main.dasm'); assert.strictEqual(run.errno, 0, run.stdout); const { errors, listings } = parseRun(run, ['main.dasm', 'sub.asm']); assert.deepStrictEqual(errors, []); // lines from the include land in the include's listing, not the main file assert.deepStrictEqual(listings['sub.asm'].lines.map((l) => l.line), [1, 2]); assert.deepStrictEqual(listings['sub.asm'].lines.map((l) => l.insns), ['a9 01', 'ea']); // and the main file resumes at the line after the include assert.deepStrictEqual(listings['main.dasm'].lines.map((l) => l.line), [4]); assert.deepStrictEqual(listings['main.dasm'].lines.map((l) => l.offset), [0xf003]); }); it('follows nested macros and REPEAT blocks', function () { const src = '\tprocessor 6502\n' + // 1 '\torg $f000\n' + // 2 '\tMAC inner\n' + // 3 '\tlda #$11\n' + // 4 '\tENDM\n' + // 5 '\tMAC outer\n' + // 6 '\tldx #$22\n' + // 7 '\tinner\n' + // 8 '\tREPEAT 2\n' + // 9 '\tnop\n' + // 10 '\tREPEND\n' + // 11 '\tENDM\n' + // 12 '\touter\n' + // 13 'done:\tldy #$33\n'; // 14 const run = runDASM({ 'src.dasm': src }, 'src.dasm'); assert.strictEqual(run.errno, 0, run.stdout); const { errors, listings } = parseRun(run, ['src.dasm']); assert.deepStrictEqual(errors, []); const lines = listings['src.dasm'].lines; // outer invoked at 13 -> ldx (7), inner's lda (4), two nops (10), then ldy (14) assert.deepStrictEqual(lines.map((l) => l.line), [13, 7, 4, 10, 10, 14]); assert.deepStrictEqual(lines.map((l) => l.insns), [null, 'a2 22', 'a9 11', 'ea', 'ea', 'a0 33']); }); it('reports an unknown mnemonic once, at its source line', function () { const src = '\tprocessor 6502\n\torg $f000 ; this is a comment\nfoo asl a\n'; const run = runDASM({ 'src.dasm': src }, 'src.dasm'); assert.notStrictEqual(run.errno, 0); const { errors, unresolved } = parseRun(run, ['src.dasm']); assert.deepStrictEqual(Object.keys(unresolved), ['a']); assert.strictEqual(errors.length, 2, JSON.stringify(errors)); for (const err of errors) { assert.strictEqual(err.path, 'src.dasm'); assert.strictEqual(err.line, 3); } assert.ok(errors.some((e) => /Unknown Mnemonic/.test(e.msg)), JSON.stringify(errors)); assert.ok(errors.some((e) => /Unresolved symbol 'a'/.test(e.msg)), JSON.stringify(errors)); }); it('keeps the fatal summary out of the way of real errors', function () { const errors: WorkerError[] = []; const fatal = parseDASMOutput( "src.dasm (3): error: Unknown Mnemonic 'asl a'.\n\nFatal assembly error: Source is not resolvable.\n", errors, {}); assert.strictEqual(fatal, 'Fatal assembly error: Source is not resolvable.'); assert.deepStrictEqual(errors, [{ path: 'src.dasm', line: 3, msg: "Unknown Mnemonic 'asl a'." }]); }); // DASM 2.20.16 asserts in ShowSymbols() when there are no symbols to show, // but only after it has written the listing and the binary, so the tool // treats the trap as non-fatal when those files are there. it('still produces a listing and a binary when it traps on an empty symbol table', function () { const run = runDASM({ 'src.dasm': '\tprocessor 6502\n\torg $f000\n\tlda #1\n' }, 'src.dasm'); assert.notStrictEqual(run.errno, 0); assert.strictEqual(run.symbols, ''); // written, but never filled in assert.deepStrictEqual(Array.from(run.output), [0xa9, 0x01]); const { errors, listings } = parseRun(run, ['src.dasm']); assert.deepStrictEqual(errors, []); assert.deepStrictEqual(listings['src.dasm'].lines.map((l) => l.line), [3]); }); it('maps a real preset onto its sources', function () { const names = ['fullgame.a', 'vcs.h', 'macro.h', 'xmacro.h']; const files = {}; files['fullgame.a'] = fs.readFileSync('./presets/vcs/examples/fullgame.a', 'utf8'); for (const inc of ['vcs.h', 'macro.h', 'xmacro.h']) { const dir = inc === 'xmacro.h' ? './presets/vcs/' : './src/worker/lib/vcs/'; files[inc] = fs.readFileSync(dir + inc, 'utf8'); } const run = runDASM(files, 'fullgame.a'); assert.strictEqual(run.errno, 0, run.stdout); const { errors, listings } = parseRun(run, names); assert.deepStrictEqual(errors, []); let mincode = Infinity; for (const path of names) { const nlines = files[path].split('\n').length; for (const l of listings[path].lines) { // every mapped line has to exist in the file it points at assert.ok(l.line >= 1 && l.line <= nlines, `${l.path || path}:${l.line} is outside ${path} (${nlines} lines)`); // and every address has to be inside the 4K cartridge assert.ok(l.offset >= 0xf000 && l.offset <= 0xffff, `${l.path || path}:${l.line} is at $${l.offset.toString(16)}`); if (l.iscode && l.offset > 0) mincode = Math.min(mincode, l.offset); } } // the origin the debugger infers from the listing assert.strictEqual(mincode, 0xf000); // macro body lines land in their file's listing too, so most of the // main source maps; in particular it must include lines past the // macros defined at 165-187 (see the regression test below) const own = listings['fullgame.a'].lines; assert.ok(own.length > 200, `only ${own.length} lines mapped`); assert.ok(Math.max(...own.map((l) => l.line)) >= 390, `latest mapped line was ${Math.max(...own.map((l) => l.line))}`); }); it('keeps a macro file\'s lines with the rest of that file', function () { // A macro defined in the main file puts its body lines in the main // file's listing. Tagging them with a path used to split that listing // so getListingForFile() returned only the macro body, hiding every // line after the macro from the editor (e.g. multisprite3.a). const src = '\tprocessor 6502\n' + // 1 '\torg $f000\n' + // 2 '\tMAC mack\n' + // 3 '\tlda #0\n' + // 4 '\tENDM\n' + // 5 'foo:\tmack\n' + // 6 '\tnop\n'; // 7 const run = runDASM({ 'src.dasm': src }, 'src.dasm'); assert.strictEqual(run.errno, 0, run.stdout); const { errors, listings } = parseRun(run, ['src.dasm']); assert.deepStrictEqual(errors, []); processListings(listings); const lst = getListingForFile(listings, 'src.dasm', 'src.dasm'); // the invocation (6), the macro body (4) and the line after it (7) assert.deepStrictEqual(lst.sourcefile.lines.map((l) => l.line), [6, 4, 7]); }); it('parses the symbol table', function () { const run = runDASM({ 'src.dasm': MACRO_SRC }, 'src.dasm'); const symbolmap = parseSymbolMap(run.symbols); assert.strictEqual(symbolmap['foo'], 0xf000); }); });