mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
import assert from "assert";
|
|
import { describe, it } from "mocha";
|
|
import { CallGraphBuilder, InsnKind } from "../../src/common/callgraph";
|
|
import { ProbeFlags } from "../../src/common/probe";
|
|
|
|
// Feed a builder the events the probe would log: before each instruction,
|
|
// the SP change the previous one made, then the instruction's address.
|
|
function run(kinds: { [pc: number]: InsnKind }, steps: [number, number][]) {
|
|
const b = new CallGraphBuilder(pc => kinds[pc] ?? 'other', pc => '$' + pc.toString(16));
|
|
let sp = -1;
|
|
for (const [pc, newsp] of steps) {
|
|
if (newsp !== sp) {
|
|
b.event(newsp < sp ? ProbeFlags.SP_PUSH : ProbeFlags.SP_POP, newsp, 0);
|
|
sp = newsp;
|
|
}
|
|
b.event(ProbeFlags.EXECUTE, pc, 0);
|
|
}
|
|
return b;
|
|
}
|
|
|
|
describe('Call graph', function () {
|
|
it('follows calls and returns, not pushes and jumps', function () {
|
|
const b = run({ 0x102: 'call', 0x205: 'return', 0x110: 'call' }, [
|
|
[0x100, 0xf004],
|
|
[0x102, 0xf004], // CALL $200
|
|
[0x200, 0xf002],
|
|
[0x201, 0xf002], // PUSH HL, then a jump: not a call
|
|
[0x202, 0xf000],
|
|
[0x300, 0xf000],
|
|
[0x204, 0xf002], // POP HL
|
|
[0x205, 0xf002], // RET
|
|
[0x103, 0xf004],
|
|
[0x110, 0xf004], // CALL C,$200, not taken: SP stays
|
|
[0x113, 0xf004],
|
|
]);
|
|
assert.deepStrictEqual(Object.keys(b.graph.calls), ['$200']);
|
|
assert.strictEqual(b.graph.calls['$200'].count, 1);
|
|
assert.deepStrictEqual(Object.keys(b.graph.calls['$200'].calls), []);
|
|
assert.strictEqual(b.stack.length, 1);
|
|
});
|
|
|
|
it('counts an interrupt as a call', function () {
|
|
const b = new CallGraphBuilder(() => 'other', pc => '$' + pc.toString(16));
|
|
b.event(ProbeFlags.SP_POP, 0x1ff, 0);
|
|
b.event(ProbeFlags.EXECUTE, 0x8000, 0);
|
|
b.event(ProbeFlags.EXECUTE, 0x8001, 0);
|
|
b.event(ProbeFlags.INTERRUPT, 0, 0);
|
|
b.event(ProbeFlags.SP_PUSH, 0x1fc, 0);
|
|
b.event(ProbeFlags.EXECUTE, 0x9000, 0);
|
|
assert.deepStrictEqual(Object.keys(b.graph.calls), ['$9000']);
|
|
});
|
|
|
|
it('counts an unreported interrupt as a call, so its return is not a new root', function () {
|
|
// an NMI: the interrupted instruction is not a call, SP drops by 3 and the PC jumps
|
|
const b = run({ 0x9002: 'return' }, [
|
|
[0x100, 0x1ff], [0x101, 0x1ff],
|
|
[0x9000, 0x1fc], [0x9001, 0x1fc], [0x9002, 0x1fc], // NMI handler, ends in RTI
|
|
[0x102, 0x1ff], [0x103, 0x1ff],
|
|
]);
|
|
assert.deepStrictEqual(Object.keys(b.graph.calls), ['$9000']);
|
|
assert.strictEqual(b.graph.calls['$9000'].count, 1);
|
|
assert.strictEqual(b.stack.length, 1);
|
|
assert.strictEqual(b.graph.$$PC, null); // the root was not re-parented
|
|
});
|
|
|
|
it('returns past where it started into a new root', function () {
|
|
const b = run({ 0x205: 'return' }, [[0x204, 0xf000], [0x205, 0xf000], [0x103, 0xf002]]);
|
|
assert.deepStrictEqual(Object.keys(b.graph.calls), ['$205']);
|
|
assert.strictEqual(b.stack.length, 1);
|
|
});
|
|
|
|
it('guesses from SP and PC when it cannot tell the instruction', function () {
|
|
const kinds = new Proxy({}, { get: () => 'unknown' }) as any;
|
|
const b = run(kinds, [[0x100, 0x1ff], [0x101, 0x1ff], [0x200, 0x1fd]]);
|
|
assert.deepStrictEqual(Object.keys(b.graph.calls), ['$200']);
|
|
});
|
|
|
|
it('charges clocks to the routine running and to its callers', function () {
|
|
const b = new CallGraphBuilder(pc => pc === 0x102 ? 'call' : pc === 0x201 ? 'return' : 'other', pc => '$' + pc.toString(16));
|
|
const step = (pc: number, sp: number, clocks: number) => {
|
|
if (sp !== (step as any).sp) b.event(sp < (step as any).sp ? ProbeFlags.SP_PUSH : ProbeFlags.SP_POP, sp, 0);
|
|
(step as any).sp = sp;
|
|
b.event(ProbeFlags.EXECUTE, pc, 0);
|
|
b.event(ProbeFlags.CLOCKS, clocks, 0);
|
|
};
|
|
(step as any).sp = -1;
|
|
step(0x100, 0xf004, 4);
|
|
step(0x102, 0xf004, 6); // CALL $200
|
|
step(0x200, 0xf002, 10);
|
|
step(0x201, 0xf002, 6); // RET
|
|
step(0x103, 0xf004, 4);
|
|
const call = b.graph.calls['$200'];
|
|
assert.strictEqual(call.self, 16);
|
|
assert.strictEqual(call.total, 16);
|
|
assert.strictEqual(b.graph.self, 4 + 6 + 4); // the CALL counts where it started; so does the RET, in $200
|
|
assert.strictEqual(b.graph.total, 30);
|
|
});
|
|
});
|