added more breakpoint conditions, updated docs

This commit is contained in:
Steven Hugg committed 2026-09-14 21:06:34 +02:00
1 parent a0c5abc8a8
commit ded0a5364e
6 files changed
+121 -61

No files matched your search

+15 -15
View File
@@ -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
View File
@@ -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) {
+15
View File
@@ -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,
};
}
-44
View File
@@ -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);
+1 -1
View File
@@ -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');
+37
View File
@@ -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', () => {