Files
8bitworkshop/gen/common/debugcontroller.js
T

411 lines
15 KiB
JavaScript

"use strict";
// The debugger every host shares: the CLI's run scripts, the VS Code debug
// adapter, and later the IDE. It runs on EmuCore, so going backwards replays
// the recording, and it knows nothing about how a host shows things.
//
// Forward commands (continue, the steps, runTo) set a goal; run() then works
// toward it a bounded number of frames at a time, so a host can keep its UI
// alive and pause between calls. Backward commands (stepBack,
// reverseContinue, seek) search the recording and finish at once.
Object.defineProperty(exports, "__esModule", { value: true });
exports.DebugController = void 0;
exports.isCallInsn = isCallInsn;
exports.isReturnInsn = isReturnInsn;
exports.buildDebugContext = buildDebugContext;
const breakpoints_1 = require("./breakpoints");
const emu_1 = require("./emu");
const emucore_1 = require("./emucore");
const projectcore_1 = require("./projectcore");
const timeline_1 = require("./timeline");
const util_1 = require("./util");
const workertypes_1 = require("./workertypes");
// bytes a PC may be past the start of its source line (as in the IDE)
const PC_LINE_LOOKBEHIND = 64;
// how many instructions a line step back may walk before giving up
const MAX_LINE_STEPS_BACK = 10000;
const CALL_INSN = /^(JSR|JSL|CALL|BSR|LBSR|RST)\b/i;
const RETURN_INSN = /^(RTS|RTL|RTI|RET|RETI|RETN)\b|^PUL[SU]\b.*\bPC\b/i;
/** True if the disassembled instruction calls a subroutine. */
function isCallInsn(line) { return CALL_INSN.test(line.trim()); }
/** True if the disassembled instruction returns from one. */
function isReturnInsn(line) { return RETURN_INSN.test(line.trim()); }
class DebugController {
constructor(core, ctx = {}) {
this.core = core;
this.resolved = [];
this.byPC = new Map();
this.goal = null;
this.sortedSymbols = null;
this.setContext(ctx);
}
/** A new build: new symbols and listings. Breakpoints resolve again. */
setContext(ctx) {
this.ctx = Object.assign({ platform: this.core.platform }, ctx);
this.sortedSymbols = null;
this.setBreakpoints(this.resolved.map(r => r.bp));
}
get context() { return this.ctx; }
/** Replace the breakpoints. Returns how each one resolved. */
setBreakpoints(bps) {
this.resolved = bps.map(bp => (0, breakpoints_1.resolveBreakpoint)(bp, this.ctx));
this.byPC.clear();
for (const r of this.resolved) {
if (!r.bp.enabled || r.error || !(r.pc >= 0))
continue;
const list = this.byPC.get(r.pc);
if (list)
list.push(r);
else
this.byPC.set(r.pc, [r]);
}
return this.resolved;
}
/** True while a forward command has not stopped yet. */
get running() { return this.goal != null; }
//// forward: set a goal, then call run()
continue() {
this.setGoal('breakpoint', () => false);
this.goal.free = true;
}
/** Run until the PC reaches `addr`, which may be where it already is. */
runTo(addr) {
this.setGoal('breakpoint', pc => pc === addr);
}
stepInstruction() {
this.requireStep();
this.setGoal('step', (_pc, first) => !first);
}
/**
* Step to the next source line, into any call on this one, or one
* instruction. Without source for the current PC, steps one instruction.
*/
stepIn(granularity = 'line') {
this.requireStep();
const line = granularity === 'line' ? this.sourceAt(this.core.getPC()) : null;
this.setGoal('step', (pc, first) => {
if (first)
return false;
if (!line)
return true;
const here = this.sourceAt(pc);
return !!here && !sameLine(here, line);
});
}
/**
* Step to the next source line, or instruction, running calls to their
* return. Without source for the current PC, steps one instruction.
*/
stepOver(granularity = 'line') {
this.requireStep();
const line = granularity === 'line' ? this.sourceAt(this.core.getPC()) : null;
let ret = null;
this.setGoal('step', (pc, first) => {
if (ret) {
if (pc !== ret.pc || this.sp() < ret.sp)
return false;
ret = null;
}
if (!first) {
if (!line)
return true;
const here = this.sourceAt(pc);
if (here && !sameLine(here, line))
return true;
}
const d = this.core.disassemble(pc);
if (d && isCallInsn(d.line))
ret = { pc: pc + d.nbytes, sp: this.sp() };
return false;
});
}
/** Run until the current subroutine returns to its caller. */
stepOut() {
this.requireStep();
const sp0 = this.sp();
let returned = false;
this.setGoal('step', pc => {
const d = this.core.disassemble(pc);
// without a disassembler, any pop past the entry level counts
if ((returned || !d) && this.sp() > sp0)
return true;
returned = !!d && isReturnInsn(d.line);
return false;
});
}
/** Stop at the next instruction boundary (or frame, where that's all there is). */
pause() {
this.goal = null;
return this.stopEvent('pause');
}
/**
* Work toward the goal for up to `maxFrames` frames. Returns how it
* stopped, or null if it is still going (or there is no goal).
*/
run(maxFrames = emucore_1.DEFAULT_MAX_FRAMES) {
const g = this.goal;
if (!g)
return null;
if (g.free && this.byPC.size === 0)
return this.runFree(maxFrames);
let stop = null;
const pred = () => {
const first = g.atStart;
g.atStart = false;
const pc = this.core.getPC();
if (!first) {
const ids = this.breakpointsAt(pc);
if (ids) {
stop = this.stopEvent('breakpoint', { breakpoints: ids });
return true;
}
}
if (g.test(pc, first)) {
stop = this.stopEvent(g.reason);
return true;
}
return false;
};
return this.stopOnHalt(() => {
this.core.runUntil(pred, maxFrames);
if (stop)
this.goal = null;
return stop;
});
}
/** Whole frames with no trap, which is as fast as the emulator runs. */
runFree(maxFrames) {
return this.stopOnHalt(() => {
for (let i = 0; i < maxFrames; i++)
this.core.advanceFrame();
return null;
});
}
/** A halt (KIL, a watchdog, a program's exit) is a stop, not an error. */
stopOnHalt(body) {
try {
return body();
}
catch (e) {
if (!(e instanceof emu_1.EmuHalt))
throw e;
this.goal = null;
return this.stopEvent(e.normal ? 'halt' : 'exception', { message: e.message });
}
}
/** run() until it stops, for hosts that can wait: null if it ran out of frames. */
runToStop(maxFrames = emucore_1.DEFAULT_MAX_FRAMES) {
const stop = this.run(maxFrames);
if (!stop)
this.goal = null;
return stop;
}
//// backward: search the recording and finish at once
/** Step back one instruction, or to the start of the previous source line. */
stepBack(granularity = 'line') {
this.requireRewind();
this.requireStep();
this.goal = null;
const line = granularity === 'line' ? this.sourceAt(this.core.getPC()) : null;
if (!line)
return this.core.stepBack() ? this.stopEvent('step') : null;
// the last moment on another line, not inside a call from this one
const sp0 = this.sp();
const start = this.core.now();
let prev = null;
if (!this.core.stepBackUntil(() => this.sp() >= sp0 && !!(prev = this.sourceAt(this.core.getPC())) && !sameLine(prev, line))) {
return null;
}
// then back to where that line started
const depth = this.sp();
for (let i = 0; i < MAX_LINE_STEPS_BACK; i++) {
const t = this.core.now();
if (!this.core.stepBackUntil(() => this.sp() >= depth))
break;
const here = this.sourceAt(this.core.getPC());
if (!here || !sameLine(here, prev)) {
this.core.seek(t);
break;
}
}
return (0, timeline_1.timestampsEqual)(this.core.now(), start) ? null : this.stopEvent('step');
}
/**
* Run backwards to the last breakpoint hit before now, or to the start of
* the recording if there is none.
*/
reverseContinue() {
this.requireRewind();
this.goal = null;
let ids = null;
if (this.byPC.size && this.core.reverseRunUntil(() => !!(ids = this.breakpointsAt(this.core.getPC())))) {
return this.stopEvent('breakpoint', { breakpoints: ids });
}
this.core.seek(this.core.history.first());
return this.stopEvent('entry', { message: 'start of recording' });
}
/** Move to a recorded moment. */
seek(t) {
this.requireRewind();
this.goal = null;
this.core.seek(t);
return this.stopEvent('goto');
}
/** Move to the start of a recorded frame, clamped to what's recorded. */
seekFrame(frame) {
this.requireRewind();
const h = this.core.history;
const f = Math.max(h.first().frame, Math.min(frame, h.last().frame));
return this.seek((0, timeline_1.timestamp)(f, 0));
}
//// where it is
location() {
const pc = this.core.getPC();
const loc = { at: this.core.now(), pc };
const source = this.sourceAt(pc);
if (source)
loc.source = source;
const symbol = pc != null ? this.symbolAt(pc) : null;
if (symbol)
loc.symbol = symbol;
return loc;
}
sourceAt(pc) {
return pc != null && this.ctx.sourceAt ? this.ctx.sourceAt(pc) : null;
}
/** The nearest symbol at or before `addr`. */
symbolAt(addr) {
if (!this.sortedSymbols) {
this.sortedSymbols = Object.entries(this.ctx.symbols || {}).sort((a, b) => a[1] - b[1]);
}
const syms = this.sortedSymbols;
let lo = 0, hi = syms.length - 1, best = -1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (syms[mid][1] <= addr) {
best = mid;
lo = mid + 1;
}
else
hi = mid - 1;
}
return best < 0 ? null : { name: syms[best][0], offset: addr - syms[best][1] };
}
//// internals
setGoal(reason, test) {
// finish a partly-run instruction first, so the start is a boundary
this.core.settle();
// run() sees the starting position first on every target that can
// stop mid-frame or rewind; on the rest the first check is a frame on
this.goal = { reason, test, atStart: this.core.supportsTrap || this.core.supportsRewind };
}
breakpointsAt(pc) {
const list = pc != null && this.byPC.get(pc);
if (!list)
return null;
let ids = null;
let state;
for (const r of list) {
if (r.condFn && !r.condFn(state = state || this.core.getCPUState()))
continue;
(ids = ids || []).push(r.bp.id);
}
return ids;
}
sp() {
const s = this.core.getCPUState();
return s && s.SP != null ? s.SP : 0;
}
stopEvent(reason, extra) {
return Object.assign({ reason, at: this.core.now(), pc: this.core.getPC() }, extra);
}
requireStep() {
if (!this.core.supportsStep)
throw new Error(`'${this.core.id}' does not support instruction stepping`);
}
requireRewind() {
if (!this.core.supportsRewind)
throw new Error(`'${this.core.id}' cannot rewind (it can't save its state)`);
}
}
exports.DebugController = DebugController;
function sameLine(a, b) {
return a.line === b.line && a.path === b.path;
}
/**
* A DebugContext from a build: source lines from its listings, and symbols.
* `paths` are the project files, which name the source a listing came from.
*/
function buildDebugContext(build) {
const listings = build.listings || {};
// listings that came over an RPC have lost their SourceFile methods
if (Object.values(listings).some(l => l.lines && !(l.sourcefile instanceof workertypes_1.SourceFile)))
(0, projectcore_1.processListings)(listings);
const paths = build.paths && build.paths.length ? build.paths : [build.mainPath];
const byName = (name) => {
const want = (0, util_1.getFilenamePrefix)((0, util_1.getFilenameForPath)((0, projectcore_1.stripLocalPath)(name, build.mainPath)));
return paths.find(p => (0, util_1.getFilenamePrefix)((0, util_1.getFilenameForPath)(p)) === want) || name;
};
const symbolAddrs = [...new Set(Object.values(build.symbolmap || {}))].sort((a, b) => a - b);
return {
symbols: build.symbolmap,
symbolsizes: build.symbolsizes,
getListingForFile: path => (0, projectcore_1.getListingForFile)(listings, path, build.mainPath),
sourceAt(pc) {
const fnStart = lastAtOrBefore(symbolAddrs, pc);
let best = null;
let bestName = '';
for (const name in listings) {
const lst = listings[name];
const files = lst.sourcefiles ? Object.entries(lst.sourcefiles) : [[name, lst.sourcefile]];
for (const [p, sf] of files) {
const loc = sf && lineAt(sf, pc, fnStart);
if (loc && (!best || Math.abs(pc - loc.offset) < Math.abs(pc - best.offset))) {
best = loc;
bestName = loc.path || p;
}
}
}
return best ? Object.assign(Object.assign({}, best), { path: byName(bestName) }) : null;
},
};
}
/**
* The source line `pc` is on. A line never reaches back across the start of
* a symbol: code between a function's label and its first listed line (the
* prologue, which compilers often leave unlisted) belongs to that first line,
* not to the end of the function before it.
*/
function lineAt(sf, pc, fnStart) {
const loc = sf.findLineForOffset(pc, PC_LINE_LOOKBEHIND);
if (!loc || fnStart == null || loc.offset >= fnStart)
return loc;
const next = firstAtOrAfter(sf.sortedOffsets, fnStart);
return next != null && next - pc <= PC_LINE_LOOKBEHIND ? sf.offset2loc.get(next) : null;
}
function lastAtOrBefore(sorted, x) {
let lo = 0, hi = sorted.length - 1, best = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (sorted[mid] <= x) {
best = sorted[mid];
lo = mid + 1;
}
else
hi = mid - 1;
}
return best;
}
function firstAtOrAfter(sorted, x) {
let lo = 0, hi = sorted.length - 1, best = null;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (sorted[mid] >= x) {
best = sorted[mid];
hi = mid - 1;
}
else
lo = mid + 1;
}
return best;
}
//# sourceMappingURL=debugcontroller.js.map