diff --git a/extension/src/buildcore.ts b/extension/src/buildcore.ts index f8118f9a..a751255f 100644 --- a/extension/src/buildcore.ts +++ b/extension/src/buildcore.ts @@ -40,6 +40,8 @@ export interface BuildOutcome { diagnostics: BuildDiagnostic[]; listings?: CodeListingMap; symbolmap?: { [sym: string]: number }; + /** symbol -> size in bytes, for the toolchains that report it */ + symbolsizes?: { [sym: string]: number }; segments?: Segment[]; /** every project path the build read */ paths: string[]; @@ -140,7 +142,7 @@ export class Builder { var r = result as any; var outcome: BuildOutcome = { success: true, tool, paths, files, diagnostics: [], - output: r.output, listings: r.listings, symbolmap: r.symbolmap, segments: r.segments, + output: r.output, listings: r.listings, symbolmap: r.symbolmap, symbolsizes: r.symbolsizes, segments: r.segments, }; this.last.set(key, outcome); return outcome; diff --git a/extension/src/extension.ts b/extension/src/extension.ts index fe2b809f..5727b3d0 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -1426,6 +1426,6 @@ async function launchForDebug(folder: vscode.WorkspaceFolder | undefined, args: if (!result.output) throw new Error('the build produced no ROM to run'); if (!await startEmulator(target, result, { paused: true })) throw new Error('the emulator failed to start; see Output'); var mainPath = path.posix.basename(target.main.path); - await emu!.call('debug', 'setBuild', { listings: result.listings, symbols: result.symbolmap, mainPath, paths: result.paths }); + await emu!.call('debug', 'setBuild', { listings: result.listings, symbols: result.symbolmap, symbolsizes: result.symbolsizes, mainPath, paths: result.paths }); return { root: path.dirname(target.main.fsPath) }; } diff --git a/src/common/debugcontroller.ts b/src/common/debugcontroller.ts index 6aa76d30..fca22567 100644 --- a/src/common/debugcontroller.ts +++ b/src/common/debugcontroller.ts @@ -40,6 +40,8 @@ export interface DebugLocation { export interface DebugContext extends BreakpointContext { /** the source line a PC belongs to */ sourceAt?(pc: number): (SourceLocation & { path: string }) | null; + /** name -> size in bytes, for the symbols the toolchain sized */ + symbolsizes?: { [name: string]: number }; } export type StepGranularity = 'line' | 'instruction'; @@ -376,6 +378,7 @@ function sameLine(a: SourceLocation, b: SourceLocation): boolean { export function buildDebugContext(build: { listings?: CodeListingMap, symbols?: { [name: string]: number }, + symbolsizes?: { [name: string]: number }, mainPath: string, paths?: string[], }): DebugContext { @@ -390,6 +393,7 @@ export function buildDebugContext(build: { const symbolAddrs = [...new Set(Object.values(build.symbols || {}))].sort((a, b) => a - b); return { symbols: build.symbols, + symbolsizes: build.symbolsizes, getListingForFile: path => getListingForFile(listings, path, build.mainPath), sourceAt(pc: number) { const fnStart = lastAtOrBefore(symbolAddrs, pc); diff --git a/src/common/symbols/symbolfile.ts b/src/common/symbols/symbolfile.ts index 2af4166d..c4f41002 100644 --- a/src/common/symbols/symbolfile.ts +++ b/src/common/symbols/symbolfile.ts @@ -19,10 +19,16 @@ export function parseSymbolFile(text: string): SymbolMap { * '.' (VICE labels). */ export function lookupSymbol(symbols: SymbolMap | null | undefined, name: string): number | undefined { + const key = resolveSymbolName(symbols, name); + return key == null ? undefined : symbols[key]; +} + +/** The key of `symbols` that `lookupSymbol` finds for `name`, if any. */ +export function resolveSymbolName(symbols: SymbolMap | null | undefined, name: string): string | undefined { if (!symbols) return undefined; - if (name in symbols) return symbols[name]; - if ('_' + name in symbols) return symbols['_' + name]; + if (name in symbols) return name; + if ('_' + name in symbols) return '_' + name; const bare = name.replace(/^\./, ''); - if (bare !== name && bare in symbols) return symbols[bare]; + if (bare !== name && bare in symbols) return bare; return undefined; } diff --git a/src/common/symbols/symbolvalue.ts b/src/common/symbols/symbolvalue.ts new file mode 100644 index 00000000..73ede55c --- /dev/null +++ b/src/common/symbols/symbolvalue.ts @@ -0,0 +1,24 @@ +import { hex } from "../util"; + +/** A symbol and the bytes at its address. */ +export interface SymbolValue { + name: string; + addr: number; + /** the first byte there */ + value: number; + /** size in bytes, if the toolchain reported one */ + size?: number; + /** the bytes there in memory order: `size` of them, up to MAX_SYMBOL_BYTES */ + bytes?: number[]; +} + +// most bytes of one symbol a client gets (a big array is just a prefix) +export const MAX_SYMBOL_BYTES = 32; + +/** `$ADDR: $XX` for one byte; `$ADDR[size]: XX XX ...` (memory order) for more. */ +export function formatSymbolValue(s: SymbolValue): string { + const bytes = s.bytes || [s.value]; + if (bytes.length === 1) return `$${hex(s.addr, 4)}: $${hex(bytes[0], 2)}`; + const more = s.size > bytes.length ? ' ...' : ''; + return `$${hex(s.addr, 4)}[${s.size}]: ${bytes.map(b => hex(b, 2)).join(' ')}${more}`; +} diff --git a/src/tools/8bws.ts b/src/tools/8bws.ts index c91cd3f9..ff863c15 100644 --- a/src/tools/8bws.ts +++ b/src/tools/8bws.ts @@ -297,7 +297,7 @@ async function openProgram(command: string, args: Args, input: string): Promise< let debugInfo: BuildInfo | undefined; if (built) { const mainPath = path.basename(input); - debugInfo = { listings: built.result.listings, symbols, mainPath, paths: [mainPath] }; + debugInfo = { listings: built.result.listings, symbols, symbolsizes: built.result.symbolsizes, mainPath, paths: [mainPath] }; } return { target, source: input, romFile, built, symbols, debugInfo }; } diff --git a/src/tools/dapsession.ts b/src/tools/dapsession.ts index 98d5e756..6f2e43ec 100644 --- a/src/tools/dapsession.ts +++ b/src/tools/dapsession.ts @@ -21,6 +21,7 @@ import type { Breakpoint } from '../common/breakpoints'; import type { StopEvent } from '../common/debugcontroller'; import { hex } from '../common/util'; import type { CallStackFrame, DebugCapabilities, DebugService, DisasmResult } from './debugservice'; +import { formatSymbolValue } from '../common/symbols/symbolvalue'; /** launch.json's arguments for a `8bitworkshop` debug session. */ export interface LaunchArgs extends DebugProtocol.LaunchRequestArguments { @@ -366,9 +367,7 @@ export class EmuDebugSession extends LoggingDebugSession { } if (ref === SYMBOLS_REF) { const syms = await this.backend.symbols(); - return syms.map(s => ({ - name: s.name, value: `$${hex(s.addr, 4)}: $${hex(s.value, 2)}`, variablesReference: 0, - })); + return syms.map(s => ({ name: s.name, value: formatSymbolValue(s), variablesReference: 0 })); } const path = this.treeRefs[ref - FIRST_TREE_REF]; if (!path) return []; diff --git a/src/tools/debugservice.ts b/src/tools/debugservice.ts index ab1c0f48..a601928c 100644 --- a/src/tools/debugservice.ts +++ b/src/tools/debugservice.ts @@ -11,7 +11,8 @@ import { compileExpression } from '../common/breakcond'; import { TreeEntry, treeChildren } from '../common/debugtree'; import { walkStack } from '../common/stackwalk'; import { buildDebugContext, DebugController, DebugLocation, StepGranularity, StopEvent } from '../common/debugcontroller'; -import { lookupSymbol } from '../common/symbols/symbolfile'; +import { resolveSymbolName } from '../common/symbols/symbolfile'; +import { formatSymbolValue, MAX_SYMBOL_BYTES, SymbolValue } from '../common/symbols/symbolvalue'; import { Granularity, Timestamp } from '../common/timeline'; import { hex } from '../common/util'; import type { CodeListingMap } from '../common/workertypes'; @@ -48,13 +49,6 @@ export interface RegisterValue { text: string; } -export interface SymbolValue { - name: string; - addr: number; - /** the byte there */ - value: number; -} - export interface CallStackFrame { /** the PC, or for callers, the address of the call */ pc: number; @@ -90,6 +84,7 @@ export interface TimelineInfo { export interface BuildInfo { listings?: CodeListingMap; symbols?: { [name: string]: number }; + symbolsizes?: { [name: string]: number }; mainPath: string; paths?: string[]; } @@ -230,12 +225,19 @@ export class DebugService { return treeChildren(tree, path); } - /** The build's symbols, by name, with the byte at each. */ + /** One symbol with the bytes at it (the first byte if its size is unknown). */ + private symbolValue(name: string): SymbolValue { + const addr = this.debug.context.symbols[name]; + const size = this.debug.context.symbolsizes?.[name] > 0 ? this.debug.context.symbolsizes[name] : undefined; + const bytes = this.readMemory(addr, Math.min(size || 1, MAX_SYMBOL_BYTES)); + return { name, addr, value: bytes[0], size, bytes }; + } + + /** The build's symbols, by name. */ symbols(): SymbolValue[] { - const syms = this.debug.context.symbols || {}; - return Object.keys(syms) + return Object.keys(this.debug.context.symbols || {}) .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) - .map(name => ({ name, addr: syms[name], value: this.target.read(syms[name] & 0xffff) & 0xff })); + .map(name => this.symbolValue(name)); } /** @@ -287,16 +289,19 @@ export class DebugService { /** * `repl`: a run-script command line (`mem $200 16`, `back 3`, `help`). * Anything else: an expression, as in a breakpoint condition (`[score]`, - * `A`, `#mem16[ptr]`), or a symbol. + * `A`, `#mem16[ptr]`), or a symbol (shown with its contents). */ evaluate(expr: string, context: string): EvalResult { expr = expr.trim(); if (context === 'repl') return this.runScript(expr); + // a symbol shows what is stored there, with `value` its address + const name = resolveSymbolName(this.debug.context.symbols, expr); + if (name != null) { + const sym = this.symbolValue(name); + return { result: formatSymbolValue(sym), value: sym.addr }; + } const ctx = makeCondContext(this.debug.context); - let value: number; - const sym = lookupSymbol(this.debug.context.symbols, expr); - if (sym != null) value = sym; - else value = compileExpression(expr, ctx)(this.target.getCPUState()); + const value = compileExpression(expr, ctx)(this.target.getCPUState()); if (typeof value !== 'number' || isNaN(value)) throw new Error(`'${expr}' has no value`); return { result: `$${hex(value, value > 0xff ? 4 : 2)} (${value})`, value }; } diff --git a/src/tools/testlib.ts b/src/tools/testlib.ts index 431c86f2..5242359f 100644 --- a/src/tools/testlib.ts +++ b/src/tools/testlib.ts @@ -36,6 +36,7 @@ export interface CompileResult { errors?: { line: number; msg: string; path?: string }[]; listings?: any; symbolmap?: any; + symbolsizes?: { [sym: string]: number }; segments?: any; params?: any; unchanged?: boolean; @@ -225,6 +226,7 @@ function workerResultToCompileResult(result: WorkerResult): CompileResult { output: result.output, listings: (result as any).listings, symbolmap: (result as any).symbolmap, + symbolsizes: (result as any).symbolsizes, segments: (result as any).segments, params: (result as any).params, }; diff --git a/src/worker/tools/sdcc.ts b/src/worker/tools/sdcc.ts index 16799901..b4ea803f 100644 --- a/src/worker/tools/sdcc.ts +++ b/src/worker/tools/sdcc.ts @@ -46,6 +46,34 @@ export function bankedAreaArgs(rels: string[], banking: ROMBanking): string[] { * above 64 KB are placed by bank, and the image grows to the next power of 2 * that holds the highest bank. */ +/** + * Guesses each symbol's size as the distance to the next symbol in its + * segment (the last one runs to the segment's end). The linker map only has + * addresses, so this is a heuristic and errs on the big side: + * - alignment padding, or a variable with no symbol of its own (a local + * static, a string literal), is counted into the symbol before it + * - a segment's last symbol also takes any unused space at its end + * - symbols sharing an address (aliases) all get the same size + * - a label inside a function or data blob ends the symbol before it, so + * that one comes out too small + * Symbols outside every segment get no size. + */ +export function inferSymbolSizes(symbolmap: { [sym: string]: number }, segments: { start: number, size: number }[]) { + // s__SEG / l__SEG are segment bounds, and the linker's '$' names are debug labels + const names = Object.keys(symbolmap).filter(n => !/^[sl]__/.test(n) && !n.includes('$')); + const sizes: { [sym: string]: number } = {}; + for (const seg of segments) { + const end = seg.start + seg.size; + const inseg = names.filter(n => symbolmap[n] >= seg.start && symbolmap[n] < end); + const addrs = [...new Set(inseg.map(n => symbolmap[n]))].sort((a, b) => a - b); + for (const n of inseg) { + const next = addrs[addrs.indexOf(symbolmap[n]) + 1] ?? end; + sizes[n] = next - symbolmap[n]; + } + } + return sizes; +} + export function parseIHX(ihx: string, rom_start: number, rom_size: number, errors: WorkerError[], banking?: ROMBanking) { var output = new Uint8Array(new ArrayBuffer(rom_size)); var upper = 0; // from type 04 (extended linear address) records @@ -327,6 +355,7 @@ export function linkSDLDZ80(step: BuildStep) { listings: listings, errors: errors, symbolmap: symbolmap, + symbolsizes: inferSymbolSizes(symbolmap, segments), segments: segments }; } diff --git a/test/unit/testdapsession.ts b/test/unit/testdapsession.ts index 7e966c4a..cd90896a 100644 --- a/test/unit/testdapsession.ts +++ b/test/unit/testdapsession.ts @@ -19,7 +19,7 @@ async function launch(args: any = {}): Promise { const target = await loadPlatform('mw8080bw'); await target.start(); await target.loadROM(build.output); - return { target, root: ROOT, debugInfo: { listings: build.listings, symbols: build.symbolmap, mainPath: 'game2.c' } }; + return { target, root: ROOT, debugInfo: { listings: build.listings, symbols: build.symbolmap, symbolsizes: { _draw_char: 4, _main: 100 }, mainPath: 'game2.c' } }; }); const c = new DapClient(new EmuDebugSession(backend)); const caps = await c.request('initialize', { adapterID: '8bitworkshop', linesStartAt1: true, columnsStartAt1: true, pathFormat: 'path' }); @@ -111,6 +111,9 @@ describe('Debug adapter', function () { const syms = await c.request('variables', { variablesReference: scopes[2].variablesReference }); const sym = syms.variables.find((v: any) => v.name === '_draw_char'); assert.ok(sym.value.includes('$' + build.symbolmap['_draw_char'].toString(16).padStart(4, '0').toUpperCase()), 'symbol shows its address'); + assert.match(sym.value, /\[4\]: ([0-9A-F]{2} ){3}[0-9A-F]{2}$/, 'a sized symbol shows all its bytes'); + const big = syms.variables.find((v: any) => v.name === '_main'); + assert.match(big.value, /\[100\]: ([0-9A-F]{2} ){31}[0-9A-F]{2} \.\.\.$/, 'a big symbol shows a prefix'); }); it('runs run-script commands in the Debug Console, and expressions in watches', async function () { @@ -120,6 +123,8 @@ describe('Debug adapter', function () { await c.event('stopped'); const hover = await c.request('evaluate', { expression: 'SP', context: 'hover' }); assert.match(hover.result, /^\$[0-9A-F]{4} \(\d+\)$/); + const symHover = await c.request('evaluate', { expression: 'draw_char', context: 'hover' }); + assert.match(symHover.result, /^\$[0-9A-F]{4}\[4\]: ([0-9A-F]{2} ){3}[0-9A-F]{2}$/, 'a symbol hovers as its contents'); const back = await c.request('evaluate', { expression: 'back 3', context: 'repl' }); assert.match(back.result, /back 3: PC=/); assert.strictEqual((await c.event('stopped')).reason, 'goto'); diff --git a/test/unit/testsdccsizes.ts b/test/unit/testsdccsizes.ts new file mode 100644 index 00000000..6eb6c52b --- /dev/null +++ b/test/unit/testsdccsizes.ts @@ -0,0 +1,20 @@ +import assert from "assert"; +import { describe, it } from "mocha"; +import { inferSymbolSizes } from "../../src/worker/tools/sdcc"; + +describe('sdcc symbol sizes', function () { + const segments = [{ start: 0x2000, size: 0x10 }, { start: 0x100, size: 0x40 }]; + + it('sizes a symbol up to the next one, and the last up to its segment end', function () { + const sizes = inferSymbolSizes({ _a: 0x2000, _b: 0x2001, _c: 0x2003 }, segments); + assert.deepStrictEqual(sizes, { _a: 1, _b: 2, _c: 0xd }); + }); + + it('gives aliases the same size, and skips bounds, debug labels and outsiders', function () { + const sizes = inferSymbolSizes({ + _a: 0x2000, _alias: 0x2000, _b: 0x2004, s__DATA: 0x2000, l__DATA: 0x10, + 'G$f$0$0': 0x2002, _rom: 0x100, _far: 0x9000, + }, segments); + assert.deepStrictEqual(sizes, { _a: 4, _alias: 4, _b: 0xc, _rom: 0x40 }); + }); +});