vscode: infer symbol sizes

This commit is contained in:
Steven Hugg committed 2026-10-01 11:06:43 +02:00
1 parent d908fcfdc4
commit 2de55caa60
12 files changed
+123 -27

No files matched your search

+3 -1
View File
@@ -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;
+1 -1
View File
@@ -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) };
}
+4
View File
@@ -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);
+9 -3
View File
@@ -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;
}
+24
View File
@@ -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
View File
@@ -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 };
}
+2 -3
View File
@@ -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
View File
@@ -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 };
}
+2
View File
@@ -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,
};
+29
View File
@@ -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
};
}
+6 -1
View File
@@ -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');
+20
View File
@@ -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 });
});
});