mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
added more breakpoint conditions, updated docs
This commit is contained in:
1 parent
a0c5abc8a8
commit
ded0a5364e
6 files changed
+121
-61
No files matched your search
+15
-15
@@ -8,8 +8,8 @@ implement `runEval` or `runToPC`.
|
||||
|
||||
| Kind | Created by | Identified by |
|
||||
| --- | --- | --- |
|
||||
| **Source** | Clicking a line in the editor gutter, or **Run To Line** | file and line number |
|
||||
| **Address** | The form in this window | a symbol, `$hex`, or decimal address |
|
||||
| **Source** | Clicking the editor gutter, or **Run To Line** | file and line number |
|
||||
| **Address** | Using the Breakpoints window | a symbol, `$hex`, or decimal address |
|
||||
|
||||
Source breakpoints are shown as red circles in the editor's gutter.
|
||||
|
||||
@@ -17,14 +17,13 @@ Address breakpoints are resolved against the debug symbol map, so a symbol from
|
||||
a linker map works as well as a raw address.
|
||||
|
||||
A breakpoint that cannot be
|
||||
resolved right now (for example a symbol that does not exist for the
|
||||
current platform) is shown with an error instead of an address.
|
||||
resolved (for example a symbol that does not exist)
|
||||
is shown with an error.
|
||||
|
||||
## Conditions
|
||||
|
||||
Any breakpoint can carry a condition expression. The breakpoint only
|
||||
stops when the expression evaluates to a non-zero value. Conditions are
|
||||
evaluated against CPU state while the emulator is running.
|
||||
A breakpoint can carry a condition expression. The breakpoint only
|
||||
stops when the expression evaluates to a non-zero value.
|
||||
|
||||
Supported values:
|
||||
|
||||
@@ -34,7 +33,12 @@ Supported values:
|
||||
| Hex number | `$1a2f`, `0x1a2f` |
|
||||
| CPU register | `PC`, `A`, `X`, `Y`, `SP`, … |
|
||||
| Debug symbol | `mainloop` |
|
||||
| Memory byte | `[$0200]` |
|
||||
| Memory byte | `[$0200]` or `#mem[$0200]` |
|
||||
| Memory word | `#mem16[$0200]` (little-endian) |
|
||||
| Video RAM byte | `#vram[$2000]` |
|
||||
| Video RAM word | `#vram16[$2000]` |
|
||||
| Current scanline | `#scanline` |
|
||||
| Current line clock | `#lineclock` |
|
||||
|
||||
Supported operators, in C-like precedence: `!` `~` `-` `+` (unary),
|
||||
`*` `/` `%`, `+` `-`, `<<` `>>`, `<` `<=` `>` `>=`, `==` `!=`, `&`,
|
||||
@@ -42,10 +46,8 @@ Supported operators, in C-like precedence: `!` `~` `-` `+` (unary),
|
||||
when it evaluates to a non-zero value.
|
||||
|
||||
For example, `A == $20 && X < 4` stops only when the accumulator holds
|
||||
`$20` and the X register is less than 4.
|
||||
|
||||
Conditions are parsed to a small closure — never `eval()` — so an
|
||||
invalid expression fails safely.
|
||||
`$20` and the X register is less than 4. A raster condition such as
|
||||
`#scanline == 222` stops only once the beam has reached scanline 222.
|
||||
|
||||
## Managing breakpoints
|
||||
|
||||
@@ -57,10 +59,8 @@ invalid expression fails safely.
|
||||
- **Edit** — click the pencil to load a breakpoint into the form;
|
||||
**Save** applies the change.
|
||||
- **Delete** — click the trash icon.
|
||||
- **Jump** — click a breakpoint's location to open the source file, or
|
||||
- **Go to Source** — click a breakpoint's location to open the source file, or
|
||||
the disassembly, at the breakpoint's address.
|
||||
|
||||
The breakpoint that the CPU is currently stopped at is highlighted.
|
||||
|
||||
Breakpoints are persisted in browser local storage, scoped to the
|
||||
current platform and project file, so they survive a reload.
|
||||
+53
-1
@@ -8,6 +8,11 @@
|
||||
// registers PC, A, X, Y, SP ... (numeric fields of the CPU state)
|
||||
// symbols resolved from the debug symbol map (baked in at compile time)
|
||||
// memory [expr] reads a byte at expr
|
||||
// spaces #mem[expr] / #ram[expr] and #vram[expr] read a byte from the
|
||||
// main or VRAM space; #mem16[expr], #vram16[expr], ... read a
|
||||
// little-endian 16-bit word
|
||||
// hardware #name reads a platform accessor (e.g. #scanline), namespaced
|
||||
// behind '#' so it can never collide with a program symbol
|
||||
// Operators (C-like precedence): ! ~ - + (unary), * / %, + -, << >>,
|
||||
// < <= > >=, == !=, &, ^, |, &&, ||, and parentheses.
|
||||
// The whole expression is true when it evaluates to non-zero.
|
||||
@@ -17,6 +22,9 @@ export interface CondContext {
|
||||
cpuFields: Set<string>; // valid register names (from getCPUState())
|
||||
symbol: (name: string) => number | undefined; // debug symbol lookup
|
||||
readMem?: (addr: number) => number | undefined;
|
||||
readVRAM?: (addr: number) => number | undefined;
|
||||
// platform accessors reachable with the '#' sigil (e.g. #scanline)
|
||||
hw?: { [name: string]: () => number | undefined };
|
||||
}
|
||||
|
||||
export type CondFn = (c: any) => boolean;
|
||||
@@ -29,7 +37,7 @@ interface Token {
|
||||
}
|
||||
|
||||
const OPERATORS2 = ['<<', '>>', '<=', '>=', '==', '!=', '&&', '||'];
|
||||
const OPERATORS1 = '+-*/%<>!~&|^()[]';
|
||||
const OPERATORS1 = '+-*/%<>!~&|^()[]#';
|
||||
|
||||
function tokenize(src: string): Token[] {
|
||||
let toks: Token[] = [];
|
||||
@@ -86,6 +94,8 @@ type Node =
|
||||
{ t: 'num', v: number } |
|
||||
{ t: 'id', name: string } |
|
||||
{ t: 'mem', a: Node } |
|
||||
{ t: 'read', space: 'mem' | 'vram', width: 1 | 2, a: Node } |
|
||||
{ t: 'hw', name: string } |
|
||||
{ t: 'un', op: string, a: Node } |
|
||||
{ t: 'bin', op: string, a: Node, b: Node };
|
||||
|
||||
@@ -185,6 +195,21 @@ class Parser {
|
||||
this.expect(']');
|
||||
return { t: 'mem', a: n };
|
||||
}
|
||||
if (t.t == 'op' && t.v == '#') {
|
||||
let id = this.next();
|
||||
if (!id || id.t != 'id') throw new Error("expected a name after '#'");
|
||||
if (this.atOp('[')) {
|
||||
this.next();
|
||||
let n = this.parseOr();
|
||||
this.expect(']');
|
||||
// #mem/#ram and #vram, optionally with a 16-bit width suffix
|
||||
let m = /^(mem|ram|vram)(16)?$/i.exec(id.v);
|
||||
if (!m) throw new Error("unknown memory space '#" + id.v + "'");
|
||||
let space: 'mem' | 'vram' = m[1].toLowerCase() == 'vram' ? 'vram' : 'mem';
|
||||
return { t: 'read', space: space, width: m[2] ? 2 : 1, a: n };
|
||||
}
|
||||
return { t: 'hw', name: id.v };
|
||||
}
|
||||
throw new Error("unexpected '" + t.v + "'");
|
||||
}
|
||||
}
|
||||
@@ -219,6 +244,33 @@ function compileNode(n: Node, ctx: CondContext): NumFn {
|
||||
return v;
|
||||
};
|
||||
}
|
||||
case 'read': {
|
||||
const a = compileNode(n.a, ctx);
|
||||
const space = n.space;
|
||||
const width = n.width;
|
||||
return (c) => {
|
||||
const rd = space == 'vram' ? ctx.readVRAM : ctx.readMem;
|
||||
if (!rd) throw new Error(space == 'vram' ? "VRAM reads not supported" : "memory reads not supported");
|
||||
const addr = a(c) & 0xffff;
|
||||
let v = rd(addr);
|
||||
if (typeof v !== 'number') throw new Error("cannot read memory");
|
||||
if (width == 2) {
|
||||
let hi = rd((addr + 1) & 0xffff);
|
||||
if (typeof hi !== 'number') throw new Error("cannot read memory");
|
||||
v = (v | (hi << 8)) & 0xffff;
|
||||
}
|
||||
return v;
|
||||
};
|
||||
}
|
||||
case 'hw': {
|
||||
const name = n.name;
|
||||
const acc = ctx.hw && ctx.hw[name];
|
||||
if (!acc) throw new Error("unknown '#" + name + "'");
|
||||
return () => {
|
||||
const v = acc();
|
||||
return typeof v === 'number' ? v : 0;
|
||||
};
|
||||
}
|
||||
case 'un': {
|
||||
const a = compileNode(n.a, ctx);
|
||||
switch (n.op) {
|
||||
|
||||
@@ -161,10 +161,25 @@ function makeCondContext(): CondContext {
|
||||
if (c) cpuFields = new Set(Object.keys(c));
|
||||
} catch (e) {
|
||||
}
|
||||
// platform accessors reachable with the '#' sigil; the value is read live
|
||||
// at eval time so it reflects the current raster position, etc.
|
||||
let hw: { [name: string]: () => number | undefined } = null;
|
||||
if (platform) {
|
||||
if (platform.getRasterScanline) {
|
||||
hw = hw || {};
|
||||
hw['scanline'] = () => platform.getRasterScanline();
|
||||
}
|
||||
if (platform.getRasterLineClock) {
|
||||
hw = hw || {};
|
||||
hw['lineclock'] = () => platform.getRasterLineClock();
|
||||
}
|
||||
}
|
||||
return {
|
||||
cpuFields,
|
||||
symbol: makeSymbolLookup(),
|
||||
readMem: platform && platform.readAddress ? (a) => platform.readAddress(a) : undefined,
|
||||
readVRAM: platform && platform.readVRAMAddress ? (a) => platform.readVRAMAddress(a) : undefined,
|
||||
hw,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1577,46 +1577,6 @@ function resetAndDebug() {
|
||||
if (wasRecording) _enableRecording();
|
||||
}
|
||||
|
||||
function _breakExpression() {
|
||||
var modal = $("#debugExprModal");
|
||||
var btn = $("#debugExprSubmit");
|
||||
var input = $("#debugExprInput");
|
||||
input.val(lastBreakExpr);
|
||||
$("#debugExprExamples").text(getDebugExprExamples());
|
||||
modal.modal('show');
|
||||
var submit = () => {
|
||||
var exprs = input.val() + "";
|
||||
modal.modal('hide');
|
||||
breakExpression(exprs);
|
||||
};
|
||||
btn.off('click').on('click', submit);
|
||||
input.off('keydown').on('keydown', (e) => {
|
||||
if (e.key == 'Enter') submit();
|
||||
});
|
||||
modal.one('shown.bs.modal', () => input.trigger('focus'));
|
||||
}
|
||||
|
||||
function getDebugExprExamples(): string {
|
||||
var state = platform.saveState && platform.saveState();
|
||||
var cpu = state.c;
|
||||
console.log(cpu, state);
|
||||
var s = '';
|
||||
if (cpu.PC) s += "c.PC == 0x" + hex(cpu.PC) + "\n";
|
||||
if (cpu.SP) s += "c.SP < 0x" + hex(cpu.SP) + "\n";
|
||||
if (cpu['HL']) s += "c.HL == 0x4000\n";
|
||||
if (platform.readAddress) s += "this.readAddress(0x1234) == 0x0\n";
|
||||
if (platform.readVRAMAddress) s += "this.readVRAMAddress(0x1234) != 0x80\n";
|
||||
if (platform['getRasterScanline']) s += "this.getRasterScanline() > 222\n";
|
||||
return s;
|
||||
}
|
||||
|
||||
function breakExpression(exprs: string) {
|
||||
var fn = new Function('c', 'return (' + exprs + ');').bind(platform);
|
||||
setupBreakpoint();
|
||||
platform.runEval(fn as DebugEvalCondition);
|
||||
lastBreakExpr = exprs;
|
||||
}
|
||||
|
||||
function updateDebugWindows() {
|
||||
if (platform.isRunning()) {
|
||||
projectWindows.tick();
|
||||
@@ -1837,10 +1797,6 @@ function setupDebugControls() {
|
||||
$("#item_reset_file").click(_revertFile);
|
||||
$("#item_rename_file").click(_renameFile);
|
||||
$("#item_delete_file").click(_deleteFile);
|
||||
if (platform.runEval)
|
||||
$("#item_debug_expr").click(_breakExpression).show();
|
||||
else
|
||||
$("#item_debug_expr").hide();
|
||||
$("#item_download_rom").click(_downloadROMImage);
|
||||
$("#item_download_file").click(_downloadSourceFile);
|
||||
$("#item_download_zip").click(_downloadProjectZipFile);
|
||||
|
||||
@@ -985,7 +985,7 @@ export class BreakpointsView implements ProjectView {
|
||||
.attr('placeholder', 'address or symbol (e.g. $8000, mainloop)')
|
||||
.val(editing && editing.bp.type == 'address' ? editing.bp.target : '');
|
||||
const condInput = $('<input type="text" class="bp-input" spellcheck="false"/>')
|
||||
.attr('placeholder', 'condition (e.g. A == $20 && X < 4)')
|
||||
.attr('placeholder', 'condition (e.g. A == $20, #scanline > 222)')
|
||||
.val(editing ? editing.bp.condition || '' : '');
|
||||
const submit = $('<button class="btn btn-default btn-xs bp-submit"/>').text(editing ? 'Save' : 'Add');
|
||||
const cancel = $('<button class="btn btn-default btn-xs bp-submit"/>').text('Cancel');
|
||||
|
||||
@@ -6,6 +6,11 @@ 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 {
|
||||
@@ -47,6 +52,38 @@ describe('Breakpoints', () => {
|
||||
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', () => {
|
||||
|
||||
Reference in new issue
Block a user