diff --git a/src/docs/breakpoints.md b/src/docs/breakpoints.md index 8f8727d6..4fd425b8 100644 --- a/src/docs/breakpoints.md +++ b/src/docs/breakpoints.md @@ -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. diff --git a/src/ide/breakcond.ts b/src/ide/breakcond.ts index b9066b28..90b58469 100644 --- a/src/ide/breakcond.ts +++ b/src/ide/breakcond.ts @@ -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; // 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) { diff --git a/src/ide/breakpoints.ts b/src/ide/breakpoints.ts index 7011d0a7..3bb338c8 100644 --- a/src/ide/breakpoints.ts +++ b/src/ide/breakpoints.ts @@ -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, }; } diff --git a/src/ide/ui.ts b/src/ide/ui.ts index e4140b70..27179b75 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -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); diff --git a/src/ide/views/debugviews.ts b/src/ide/views/debugviews.ts index 9a87f0d3..a68dbb50 100644 --- a/src/ide/views/debugviews.ts +++ b/src/ide/views/debugviews.ts @@ -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 = $('') - .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 = $('