mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
204 lines
9.2 KiB
TypeScript
204 lines
9.2 KiB
TypeScript
import { describe } from "mocha";
|
|
import { compileCondition, parseTarget } from "../../src/common/breakcond";
|
|
import { Breakpoint, BreakpointContext, canUseBreakpoints, resolveBreakpoint } from "../../src/common/breakpoints";
|
|
import { lookupSymbol, parseSymbolFile } from "../../src/common/symbols/symbolfile";
|
|
import { SourceFile } from "../../src/common/workertypes";
|
|
import assert from "assert";
|
|
|
|
const ctx = {
|
|
cpuFields: new Set(['PC', 'A', 'X', 'Y', 'SP']),
|
|
symbol: (name: string) => ({ mainloop: 0x800, foo: 0x20 } as { [k: string]: number })[name],
|
|
readMem: (a: number) => (a == 0x10 ? 0x2a : 0),
|
|
readVRAM: (a: number) => (a == 0x2000 ? 0x80 : 0),
|
|
hw: {
|
|
scanline: () => 150,
|
|
lineclock: () => 40,
|
|
},
|
|
};
|
|
|
|
function evalc(src: string, c: any): boolean {
|
|
return compileCondition(src, ctx)(c);
|
|
}
|
|
|
|
describe('Breakpoints', () => {
|
|
|
|
it('should evaluate register comparisons', () => {
|
|
let c = { PC: 0x800, A: 5, X: 3, Y: 0, SP: 0x1ff };
|
|
assert.equal(evalc('A == 5', c), true);
|
|
assert.equal(evalc('A != 5', c), false);
|
|
assert.equal(evalc('A < 5', c), false);
|
|
assert.equal(evalc('A <= 5', c), true);
|
|
assert.equal(evalc('X > 2 && X < 4', c), true);
|
|
assert.equal(evalc('A == 5 && X == 4', c), false);
|
|
assert.equal(evalc('A == 5 || X == 4', c), true);
|
|
assert.equal(evalc('!(A == 5)', c), false);
|
|
});
|
|
|
|
it('should parse numbers', () => {
|
|
let c = { PC: 0, A: 0xff };
|
|
assert.equal(evalc('A == $ff', c), true);
|
|
assert.equal(evalc('A == 0xff', c), true);
|
|
assert.equal(evalc('A == 255', c), true);
|
|
assert.equal(evalc('A == $FF', c), true);
|
|
});
|
|
|
|
it('should resolve symbols at compile time', () => {
|
|
let c = { PC: 0x800, A: 0 };
|
|
assert.equal(evalc('PC == mainloop', c), true);
|
|
assert.equal(evalc('A == foo', c), false);
|
|
assert.throws(() => compileCondition('A == bar', ctx));
|
|
});
|
|
|
|
it('should support memory reads', () => {
|
|
let c = { PC: 0, A: 0x2a };
|
|
assert.equal(evalc('[0x10] == $2a', c), true);
|
|
assert.equal(evalc('A == [$10]', c), true);
|
|
assert.equal(evalc('[foo] == 0', c), true); // foo is at $20, memory there reads 0
|
|
assert.equal(evalc('[0x99] == 0', c), true);
|
|
// explicit main-memory space is a synonym for plain [expr]
|
|
assert.equal(evalc('#mem[0x10] == $2a', c), true);
|
|
assert.equal(evalc('#ram[0x10] == $2a', c), true);
|
|
});
|
|
|
|
it('should support VRAM reads', () => {
|
|
let c = { PC: 0 };
|
|
assert.equal(evalc('#vram[0x2000] == $80', c), true);
|
|
assert.equal(evalc('#vram[0x2000] != $80', c), false);
|
|
assert.equal(evalc('#vram[0x9999] == 0', c), true);
|
|
});
|
|
|
|
it('should support 16-bit reads', () => {
|
|
// little-endian: $10 holds $2a, $11 holds $01 -> $012a
|
|
let ctx16 = { ...ctx, readMem: (a: number) => (a == 0x10 ? 0x2a : a == 0x11 ? 0x01 : 0) };
|
|
let c = { PC: 0 };
|
|
assert.equal(compileCondition('#mem16[0x10] == $012a', ctx16)(c), true);
|
|
assert.equal(compileCondition('#ram16[0x10] == $012a', ctx16)(c), true);
|
|
assert.equal(evalc('#mem16[0x99] == $0000', c), true); // both bytes read 0
|
|
assert.equal(evalc('#vram16[0x2000] == $0080', c), true);
|
|
});
|
|
|
|
it('should support hardware accessors', () => {
|
|
let c = { PC: 0 };
|
|
assert.equal(evalc('#scanline == 150', c), true);
|
|
assert.equal(evalc('#scanline > 100 && #lineclock < 50', c), true);
|
|
});
|
|
|
|
it('should reject unknown hardware accessors and spaces', () => {
|
|
assert.throws(() => compileCondition('#foo == 1', ctx));
|
|
assert.throws(() => compileCondition('#bogus[0] == 1', ctx));
|
|
assert.throws(() => compileCondition('# == 1', ctx));
|
|
});
|
|
|
|
it('should support arithmetic and bitwise ops', () => {
|
|
let c = { PC: 0, A: 0x0f, X: 2 };
|
|
assert.equal(evalc('A + 1 == $10', c), true);
|
|
assert.equal(evalc('A * 2 == $1e', c), true);
|
|
assert.equal(evalc('A / 4 == 3', c), true);
|
|
assert.equal(evalc('A % 4 == 3', c), true);
|
|
assert.equal(evalc('(A & $0f) == $0f', c), true);
|
|
assert.equal(evalc('(A | $f0) == $ff', c), true);
|
|
assert.equal(evalc('(A ^ $0f) == 0', c), true);
|
|
assert.equal(evalc('A & ($0f == $0f)', c), true); // == binds tighter than &
|
|
assert.equal(evalc('1 << X == 4', c), true);
|
|
assert.equal(evalc('A >> 2 == 3', c), true);
|
|
assert.equal(evalc('~A == -$10', c), true);
|
|
assert.equal(evalc('-A == -15', c), true);
|
|
});
|
|
|
|
it('should support parentheses and precedence', () => {
|
|
let c = { PC: 0, A: 3, X: 4 };
|
|
assert.equal(evalc('(A + 1) * X == $10', c), true);
|
|
assert.equal(evalc('A + 1 * X == 7', c), true);
|
|
assert.equal(evalc('A == 3 && X == 4 || A == 9', c), true);
|
|
});
|
|
|
|
it('should not throw at eval time', () => {
|
|
let c = { PC: 0, A: 0 };
|
|
// division by zero, unreadable memory: condition is just false
|
|
assert.equal(evalc('A / 0 == 1', c), false);
|
|
assert.equal(evalc('A % 0 == 1', c), false);
|
|
});
|
|
|
|
it('should reject bad syntax', () => {
|
|
assert.throws(() => compileCondition('A ==', ctx));
|
|
assert.throws(() => compileCondition('A == (5', ctx));
|
|
assert.throws(() => compileCondition('A @ 5', ctx));
|
|
assert.throws(() => compileCondition('== 5', ctx));
|
|
assert.throws(() => compileCondition('', ctx));
|
|
assert.throws(() => compileCondition('A == 5 3', ctx));
|
|
});
|
|
|
|
it('should reject unknown identifiers', () => {
|
|
assert.throws(() => compileCondition('QQ == 5', ctx));
|
|
// identifiers that look like symbols but aren't in the map either
|
|
assert.throws(() => compileCondition('[QQQ] == 5', ctx));
|
|
});
|
|
|
|
it('should parse targets', () => {
|
|
const sym = (name: string) => ({ mainloop: 0x800 } as { [k: string]: number })[name];
|
|
assert.equal(parseTarget('$1234', sym).pc, 0x1234);
|
|
assert.equal(parseTarget('0xABC', sym).pc, 0xabc);
|
|
assert.equal(parseTarget('48879', sym).pc, 0xbeef);
|
|
assert.equal(parseTarget('mainloop', sym).pc, 0x800);
|
|
assert.ok(parseTarget('nosuch', sym).error);
|
|
assert.ok(parseTarget('', sym).error);
|
|
assert.ok(parseTarget('12ab', sym).error); // neither number nor symbol
|
|
});
|
|
});
|
|
|
|
describe('Breakpoint resolution (shared by IDE, CLI, debug adapter)', () => {
|
|
|
|
const symbols = { _main: 0x1234, mainloop: 0x800, vblank: 0x900 };
|
|
const sourcefile = new SourceFile([{ line: 10, offset: 0x1234 } as any], '');
|
|
const ctx: BreakpointContext = {
|
|
symbols,
|
|
getListingForFile: (path) => path == 'game.c' ? { lines: [], sourcefile } as any : undefined,
|
|
platform: {
|
|
getCPUState: () => ({ PC: 0x1234, A: 7 }),
|
|
readAddress: (a: number) => (a == 0x10 ? 0x2a : 0),
|
|
} as any,
|
|
};
|
|
const bp = (patch: Partial<Breakpoint>): Breakpoint => ({ id: 1, type: 'address', enabled: true, ...patch });
|
|
|
|
it('should resolve a symbol, with or without the C underscore', () => {
|
|
assert.equal(resolveBreakpoint(bp({ target: 'mainloop' }), ctx).pc, 0x800);
|
|
assert.equal(resolveBreakpoint(bp({ target: '_main' }), ctx).pc, 0x1234);
|
|
assert.equal(resolveBreakpoint(bp({ target: 'main' }), ctx).pc, 0x1234);
|
|
assert.ok(resolveBreakpoint(bp({ target: 'nosuch' }), ctx).error);
|
|
});
|
|
it('should resolve a source line through the listing', () => {
|
|
assert.equal(resolveBreakpoint(bp({ type: 'source', file: 'game.c', line: 10 }), ctx).pc, 0x1234);
|
|
assert.equal(resolveBreakpoint(bp({ type: 'source', file: 'game.c', line: 11 }), ctx).error, 'line has no code');
|
|
assert.equal(resolveBreakpoint(bp({ type: 'source', file: 'other.c', line: 10 }), ctx).error, 'no debug info (build first?)');
|
|
});
|
|
it('should compile a condition against the host platform', () => {
|
|
let r = resolveBreakpoint(bp({ target: '$800', condition: 'A == 7 && [$10] == 42' }), ctx);
|
|
assert.equal(r.pc, 0x800);
|
|
assert.equal(r.condFn({ A: 7 }), true);
|
|
assert.equal(r.condFn({ A: 6 }), false);
|
|
});
|
|
it('should work without a platform or listings', () => {
|
|
assert.equal(resolveBreakpoint(bp({ target: '$c000' }), {}).pc, 0xc000);
|
|
assert.ok(resolveBreakpoint(bp({ type: 'source', file: 'game.c', line: 10 }), {}).error);
|
|
});
|
|
it('should report whether a platform can stop at breakpoints', () => {
|
|
assert.equal(canUseBreakpoints(null), false);
|
|
assert.equal(canUseBreakpoints({ runEval: () => { } } as any), true);
|
|
});
|
|
});
|
|
|
|
describe('Symbol files', () => {
|
|
it('should parse ca65 and VICE label files', () => {
|
|
let syms = parseSymbolFile('main = $1234 ;\nal 00C000 .vblank\nadd_label 0800 loop\n');
|
|
assert.deepEqual(syms, { main: 0x1234, vblank: 0xc000, loop: 0x800 });
|
|
});
|
|
it('should look up names as a user types them', () => {
|
|
let syms = { _main: 1, loop: 2 };
|
|
assert.equal(lookupSymbol(syms, '_main'), 1);
|
|
assert.equal(lookupSymbol(syms, 'main'), 1);
|
|
assert.equal(lookupSymbol(syms, '.loop'), 2);
|
|
assert.equal(lookupSymbol(syms, 'nope'), undefined);
|
|
assert.equal(lookupSymbol(null, 'main'), undefined);
|
|
});
|
|
});
|