All files / src tight-loop.js

94.87% Statements 37/39
96.96% Branches 32/33
100% Functions 3/3
92.85% Lines 26/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67  5x   5x                               1121x 1121x 861x 806x 633x 596x 1121x                       511x 978x 978x 514x 514x 47x 467x                   47x 47x 47x 143x 143x 137x 137x 137x 98x 96x        
/** Furthest, in bytes, a tight loop's branch back can be from the instruction it goes back to. */
const MaxLoopBytes = 32;
 
const DirectModes = new Set(["implied", "imm", "zp", "abs", "branch", "zp,branch"]);
 
/**
 * @typedef {object} TightLoop
 * @property {number} start the address of its first instruction
 * @property {number} end the address of the branch back to `start`
 * @property {number[]} reads the addresses it reads, each once, in the order it first reads them
 */
 
/**
 * The instruction at `address`, as far as a tight loop cares: where it goes next, where it may branch, and what it
 * reads. Undefined for one no tight loop runs: a write, a call, a return, an indexed or indirect operand, or one
 * that would change something more if run again at once.
 * @param {ReturnType<typeof import("./6502.opcodes.js").Cpu6502>["disassembler"]} disassembler
 */
function waitingInstruction(disassembler, address) {
    const { mnemonic, mode, accesses, flow, next, address: operand, target } = disassembler.instructionAt(address);
    if (next > 0xffff || accesses.includes("write")) return undefined;
    if (flow === "jump" && mode === "abs") return { next, target, reads: [], unconditional: true };
    if (!(flow === "next" || flow === "branch") || !DirectModes.has(mode)) return undefined;
    if (!disassembler.isIdempotent(disassembler.cpu.peekmem(address))) return undefined;
    const reads = accesses.includes("read") ? [operand] : [];
    return { next, target, reads, unconditional: mnemonic === "BRA" };
}
 
/**
 * The tight loop the instruction at `pc` is part of, if it is in one: a short run of instructions closed by a branch
 * back, each of which writes nothing, calls nothing and settles, changing nothing more when run again at once, so
 * that the loop waits for what it reads to be changed by a device or an interrupt. Found from the code alone.
 * @param {ReturnType<typeof import("./6502.opcodes.js").Cpu6502>["disassembler"]} disassembler the CPU's
 * @param {number} pc
 * @returns {TightLoop | undefined}
 */
export function tightLoopAt(disassembler, pc) {
    for (let address = pc; address - pc <= MaxLoopBytes;) {
        const instruction = waitingInstruction(disassembler, address);
        if (!instruction) return undefined;
        const { target } = instruction;
        if (target !== undefined && target <= pc && address - target <= MaxLoopBytes)
            return loopFrom(disassembler, target, address, pc);
        address = instruction.next;
    }
    return undefined;
}
 
/**
 * The loop from `start` to the branch back at `end`, if every instruction of it on the way through `pc` waits and
 * none jumps away before `end`.
 */
function loopFrom(disassembler, start, end, pc) {
    const reads = new Set();
    let passesPc = false;
    for (let address = start; address <= end;) {
        const instruction = waitingInstruction(disassembler, address);
        if (!instruction) return undefined;
        passesPc ||= address === pc;
        for (const read of instruction.reads) reads.add(read);
        if (address === end) return passesPc ? { start, end, reads: [...reads] } : undefined;
        if (instruction.unconditional) return undefined;
        address = instruction.next;
    }
    return undefined;
}