mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
vscode: infer symbol sizes
This commit is contained in:
1 parent
d908fcfdc4
commit
2de55caa60
12 files changed
+123
-27
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
+1
-1
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
+22
-17
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ async function launch(args: any = {}): Promise<DapClient> {
|
||||
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');
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user