uae-dap
Version:
Debug Adapter Protocol for Amiga development with FS-UAE or WinUAE
1,134 lines (1,133 loc) • 50.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScopeType = void 0;
const debugadapter_1 = require("@vscode/debugadapter");
const expression_eval_1 = require("expression-eval");
const hardware_1 = require("./hardware");
const disassembly_1 = require("./disassembly");
const strings_1 = require("./utils/strings");
const registers_1 = require("./registers");
var ScopeType;
(function (ScopeType) {
ScopeType[ScopeType["Registers"] = 0] = "Registers";
ScopeType[ScopeType["Segments"] = 1] = "Segments";
ScopeType[ScopeType["Symbols"] = 2] = "Symbols";
ScopeType[ScopeType["StatusRegister"] = 3] = "StatusRegister";
ScopeType[ScopeType["Expression"] = 4] = "Expression";
ScopeType[ScopeType["Custom"] = 5] = "Custom";
ScopeType[ScopeType["Vectors"] = 6] = "Vectors";
ScopeType[ScopeType["SourceConstants"] = 7] = "SourceConstants";
})(ScopeType = exports.ScopeType || (exports.ScopeType = {}));
/**
* Wrapper to interact with running Program
*/
class VariableManager {
constructor(gdb, sourceMap, constantResolver, memoryFormats = {}) {
this.gdb = gdb;
this.sourceMap = sourceMap;
this.constantResolver = constantResolver;
this.memoryFormats = memoryFormats;
this.scopes = new debugadapter_1.Handles();
/** Variables lookup by handle */
this.referencedVariables = new Map();
/** Store format options for specific variables */
this.variableFormatterMap = new Map();
}
/**
* Read memory from address
*
* @param length Length of data to read in bytes
*/
async getMemory(address, length = 4) {
const hex = await this.gdb.readMemory(address, length);
return parseInt(hex, 16);
}
/**
* Get scopes for frame ID
*/
getScopes(frameId) {
return [
new debugadapter_1.Scope("Registers", this.scopes.create({ type: ScopeType.Registers, frameId }), false),
new debugadapter_1.Scope("Symbols", this.scopes.create({ type: ScopeType.Symbols, frameId }), true),
new debugadapter_1.Scope("Constants", this.scopes.create({ type: ScopeType.SourceConstants, frameId }), true),
new debugadapter_1.Scope("Segments", this.scopes.create({ type: ScopeType.Segments, frameId }), true),
new debugadapter_1.Scope("Custom", this.scopes.create({ type: ScopeType.Custom, frameId }), true),
new debugadapter_1.Scope("Vectors", this.scopes.create({ type: ScopeType.Vectors, frameId }), true),
];
}
// Variables:
/**
* Get object containing all variables and constants
*/
async getVariables(frameId) {
return this.gdb.withFrame(frameId, async () => {
const registers = await this.gdb.getRegisters();
const namedRegisters = (0, registers_1.nameRegisters)(registers);
const registerEntries = namedRegisters.reduce((acc, v) => {
acc[v.name] = v.value;
// Store size/sign fields in prefixed object
// The prefix will be stripped out later when evaluating the expression
if (v.name.match(/^[ad]/i)) {
acc["__OBJ__" + v.name] = this.registerFields(v.value);
}
return acc;
}, {});
const sourceConstants = await this.getSourceConstants();
return {
...hardware_1.customRegisterAddresses,
...this.sourceMap.getSymbols(),
...sourceConstants,
...registerEntries,
};
});
}
async getCompletions(text, frameId) {
const words = text.split(/[^\w.]/);
const lastWord = words.pop();
if (!lastWord) {
return [];
}
if (lastWord?.includes(".")) {
const parts = lastWord.split(".");
const vars = [
{ label: "b", detail: "Byte value" },
{ label: "bs", detail: "Byte value signed" },
{ label: "w", detail: "Word value" },
{ label: "ws", detail: "Word value signed" },
{ label: "l", detail: "Longword value" },
{ label: "ls", detail: "LongWord value signed" },
];
return vars.filter((v) => v.label.startsWith(parts[1]));
}
const sourceConstants = await this.getSourceConstants();
return this.gdb.withFrame(frameId, async () => {
const registers = await this.gdb.getRegisters();
const namedRegisters = (0, registers_1.nameRegisters)(registers);
const vars = [
...Object.values(hardware_1.customRegisterNames).map((label) => ({
label,
detail: "Custom",
})),
...Object.keys(this.sourceMap.getSymbols()).map((label) => ({
label,
detail: "Symbol",
})),
...Object.keys(sourceConstants).map((label) => ({
label,
detail: "Constant",
})),
...namedRegisters.map((reg) => ({
label: reg.name,
detail: "Register",
})),
];
return vars.filter((v) => v.label.startsWith(lastWord));
});
}
registerFields(value) {
const b = value & 0xff;
const bs = b >= 0x80 ? b - 0x100 : b;
const w = value & 0xffff;
const ws = w >= 0x8000 ? w - 0x10000 : w;
const l = value & 0xffffffff;
const ls = l >= 0x80000000 ? l - 0x100000000 : l;
return { b, bs, w, ws, l, ls };
}
/**
* Lazy load constants from parsed source files
*/
async getSourceConstants() {
if (this.sourceConstants) {
return this.sourceConstants;
}
const sourceFiles = this.sourceMap.getSourceFiles();
debugadapter_1.logger.log("Getting constants from source files: " + sourceFiles.join(", "));
const sourceConstants = await this.constantResolver?.getSourceConstants(sourceFiles);
this.sourceConstants = sourceConstants ?? {};
return this.sourceConstants;
}
/**
* Retrieve variables for a given variable reference i.e. scope
*/
async getVariablesByReference(variablesReference) {
// Try to look up stored reference
const variables = this.referencedVariables.get(variablesReference);
if (variables) {
return variables;
}
// Get reference info in order to populate variables
const { type, frameId } = this.getScopeReference(variablesReference);
switch (type) {
case ScopeType.Registers:
return this.getRegisterVariables(frameId);
case ScopeType.Segments:
return this.getSegmentVariables();
case ScopeType.Symbols:
return this.getSymbolVariables();
case ScopeType.Custom:
return this.getCustomVariables(frameId);
case ScopeType.Vectors:
return this.getVectorVariables();
case ScopeType.SourceConstants:
return this.getSourceConstantVariables();
}
throw new Error("Invalid reference");
}
/**
* Get information about the scope associated with a variables reference
*/
getScopeReference(variablesReference) {
const scopeRef = this.scopes.get(variablesReference);
if (!scopeRef) {
throw new Error("Reference not found");
}
return scopeRef;
}
async getRegisterVariables(frameId) {
return this.gdb.withFrame(frameId, async () => {
const registers = await this.gdb.getRegisters();
const namedRegisters = (0, registers_1.nameRegisters)(registers);
// Stack register properties go in their own variables array to be fetched later by reference
const sr = namedRegisters
.filter(({ name }) => name.startsWith("SR_"))
.map(({ name, value }) => ({
name: name.substring(3),
type: "register",
value: this.formatVariable(name, value, strings_1.NumberFormat.DECIMAL),
variablesReference: 0,
memoryReference: value.toString(),
}));
const srScope = this.scopes.create({
type: ScopeType.StatusRegister,
frameId,
});
this.referencedVariables.set(srScope, sr);
// All other registers returned
return namedRegisters
.filter(({ name }) => !name.startsWith("SR_"))
.map(({ name, value }) => {
let formatted = this.formatVariable(name, value, strings_1.NumberFormat.HEXADECIMAL, 4);
// Add offset to address registers
if (name.startsWith("a") || name === "pc") {
const offset = this.symbolOffset(value);
if (offset) {
formatted += ` (${offset})`;
}
}
let variablesReference = 0;
if (name.startsWith("sr")) {
// Reference to stack register fields
variablesReference = srScope;
}
else if (name !== "pc") {
// Size / sign variants as fields
const fields = this.registerFields(value);
const fieldVars = [
{
name: "b",
type: "register",
value: this.formatVariable("b", fields.b, strings_1.NumberFormat.HEXADECIMAL, 1),
variablesReference: 0,
},
{
name: "bs",
type: "register",
value: this.formatVariable("bs", fields.bs, strings_1.NumberFormat.HEXADECIMAL, 1),
variablesReference: 0,
},
{
name: "w",
type: "register",
value: this.formatVariable("w", fields.w, strings_1.NumberFormat.HEXADECIMAL, 2),
variablesReference: 0,
},
{
name: "ws",
type: "register",
value: this.formatVariable("ws", fields.ws, strings_1.NumberFormat.HEXADECIMAL, 2),
variablesReference: 0,
},
{
name: "l",
type: "register",
value: this.formatVariable("l", fields.l, strings_1.NumberFormat.HEXADECIMAL, 4),
variablesReference: 0,
},
{
name: "ls",
type: "register",
value: this.formatVariable("ls", fields.ls, strings_1.NumberFormat.HEXADECIMAL, 4),
variablesReference: 0,
},
];
variablesReference = this.scopes.create({
type: ScopeType.Registers,
frameId,
});
this.referencedVariables.set(variablesReference, fieldVars);
}
return {
name,
type: "register",
value: formatted,
variablesReference,
memoryReference: value.toString(),
};
});
});
}
getSegmentVariables() {
const segments = this.sourceMap.getSegmentsInfo();
return segments.map((s) => {
const name = s.name;
return {
name,
type: "segment",
value: `${this.formatVariable(name, s.address, strings_1.NumberFormat.HEXADECIMAL, 4)} {size:${s.size}}`,
variablesReference: 0,
memoryReference: s.address.toString(),
};
});
}
getSymbolVariables() {
const symbols = this.sourceMap.getSymbols();
return Object.keys(symbols)
.sort(strings_1.compareStringsLowerCase)
.map((name) => ({
name,
type: "symbol",
value: this.formatVariable(name, symbols[name], strings_1.NumberFormat.HEXADECIMAL, 4),
variablesReference: 0,
memoryReference: symbols[name].toString(),
}));
}
async getVectorVariables() {
const memory = await this.gdb.readMemory(0, 0xc0);
const chunks = (0, strings_1.chunk)(memory.toString(), 8).map((chunk) => parseInt(chunk, 16));
return hardware_1.vectors
.map((name, i) => {
if (!name)
return;
let value = this.formatVariable(name, chunks[i], strings_1.NumberFormat.HEXADECIMAL, 4);
const offset = this.symbolOffset(chunks[i]);
if (offset) {
value += ` (${offset})`;
}
return {
name: "0x" + (i * 4).toString(16).padStart(2, "0") + " " + name,
value,
variablesReference: 0,
memoryReference: (i * 4).toString(16),
};
})
.filter(Boolean);
}
async getSourceConstantVariables() {
const consts = await this.getSourceConstants();
return Object.keys(consts).map((name) => ({
name,
value: this.formatVariable(name, consts[name], strings_1.NumberFormat.HEXADECIMAL),
variablesReference: 0,
}));
}
async getCustomVariables(frameId) {
// Read memory starting at $dff000 and chunk into words
const memory = await this.gdb.readMemory(hardware_1.CUSTOM_BASE, 0x1fe);
const chunks = (0, strings_1.chunk)(memory.toString(), 4);
// Unwanted / duplicate registers to skip
const ignorenames = [
"RESERVED",
// Duplicate {REGNAME}R/{REGNAME}W - Just show the read value
"HHPOSW",
"VHPOSW",
"VPOSW",
// Duplicate {REGNAME}/{REGNAME}R - emulator is able to read from the actual register
"ADKCONR",
"DMACONR",
"DSKDATR",
"INTENAR",
"POTGOR",
"SERDATR",
];
// Build key/value map of all custom register variables
const values = chunks.reduce((acc, value, index) => {
const address = index * 2 + hardware_1.CUSTOM_BASE;
const name = hardware_1.customRegisterNames[address];
if (name && !ignorenames.includes(name)) {
acc[name] = value;
}
return acc;
}, {});
// Fields for registers which contain values in specific bytes ranges:
const fields = {
ADKCON: this.scopes.create({ type: ScopeType.Custom, frameId }),
BEAMCON0: this.scopes.create({ type: ScopeType.Custom, frameId }),
BLTSIZE: this.scopes.create({ type: ScopeType.Custom, frameId }),
BPLCON0: this.scopes.create({ type: ScopeType.Custom, frameId }),
BPLCON1: this.scopes.create({ type: ScopeType.Custom, frameId }),
BPLCON2: this.scopes.create({ type: ScopeType.Custom, frameId }),
BPLCON3: this.scopes.create({ type: ScopeType.Custom, frameId }),
BPLCON4: this.scopes.create({ type: ScopeType.Custom, frameId }),
CLXCON: this.scopes.create({ type: ScopeType.Custom, frameId }),
CLXCON2: this.scopes.create({ type: ScopeType.Custom, frameId }),
COPCON: this.scopes.create({ type: ScopeType.Custom, frameId }),
DDFSTRT: this.scopes.create({ type: ScopeType.Custom, frameId }),
DDFSTOP: this.scopes.create({ type: ScopeType.Custom, frameId }),
DIWSTRT: this.scopes.create({ type: ScopeType.Custom, frameId }),
DIWSTOP: this.scopes.create({ type: ScopeType.Custom, frameId }),
DMACON: this.scopes.create({ type: ScopeType.Custom, frameId }),
INTENA: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR0POS: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR0CTL: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR1POS: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR1CTL: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR2POS: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR2CTL: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR3POS: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR3CTL: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR4POS: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR4CTL: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR5POS: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR5CTL: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR6POS: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR6CTL: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR7POS: this.scopes.create({ type: ScopeType.Custom, frameId }),
SPR7CTL: this.scopes.create({ type: ScopeType.Custom, frameId }),
};
this.referencedVariables.set(fields.ADKCON, [
this.byteReg(values.ADKCON, "PRECOMP", 14, 13),
this.boolReg(values.ADKCON, "MFMPREC", 12),
this.boolReg(values.ADKCON, "UARTBRK", 11),
this.boolReg(values.ADKCON, "WORDSYNC", 10),
this.boolReg(values.ADKCON, "MSBSYNC", 9),
this.boolReg(values.ADKCON, "FAST", 8),
this.boolReg(values.ADKCON, "USE3PN", 7),
this.boolReg(values.ADKCON, "USE2P3", 6),
this.boolReg(values.ADKCON, "USE1P2", 5),
this.boolReg(values.ADKCON, "USE0P1", 4),
this.boolReg(values.ADKCON, "USE3VN", 3),
this.boolReg(values.ADKCON, "USE2V3", 2),
this.boolReg(values.ADKCON, "USE1V2", 1),
this.boolReg(values.ADKCON, "USE0V1", 0),
]);
this.referencedVariables.set(fields.BEAMCON0, [
this.boolReg(values.BEAMCON0, "HARDDIS", 14),
this.boolReg(values.BEAMCON0, "LPENDIS", 13),
this.boolReg(values.BEAMCON0, "VARVBEN", 12),
this.boolReg(values.BEAMCON0, "LOLDIS", 11),
this.boolReg(values.BEAMCON0, "CSCBEN", 10),
this.boolReg(values.BEAMCON0, "VARVSYEN", 9),
this.boolReg(values.BEAMCON0, "VARHSYEN", 8),
this.boolReg(values.BEAMCON0, "VARBEAMEN", 7),
this.boolReg(values.BEAMCON0, "DUAL", 6),
this.boolReg(values.BEAMCON0, "PAL", 5),
this.boolReg(values.BEAMCON0, "VARCSYEN", 4),
this.boolReg(values.BEAMCON0, "CSYTRUE", 2),
this.boolReg(values.BEAMCON0, "VSYTRUE", 1),
this.boolReg(values.BEAMCON0, "HSYTRUE", 0),
]);
this.referencedVariables.set(fields.BLTSIZE, [
this.wordReg(values.BLTSIZE, "H", 15, 6),
this.byteReg(values.BLTSIZE, "W", 5, 0),
]);
this.referencedVariables.set(fields.BPLCON0, [
this.boolReg(values.BPLCON0, "HIRES", 15),
this.byteReg(values.BPLCON0, "BPU", 14, 12),
this.boolReg(values.BPLCON0, "HOMOD", 11),
this.boolReg(values.BPLCON0, "DBLPF", 10),
this.boolReg(values.BPLCON0, "COLOR", 9),
this.boolReg(values.BPLCON0, "GAUD", 8),
this.boolReg(values.BPLCON0, "UHRES", 7),
this.boolReg(values.BPLCON0, "SHRES", 6),
this.boolReg(values.BPLCON0, "BYPASS", 5),
this.boolReg(values.BPLCON0, "BPU2", 4),
this.boolReg(values.BPLCON0, "LPEN", 3),
this.boolReg(values.BPLCON0, "LACE", 2),
this.boolReg(values.BPLCON0, "ERSY", 1),
this.boolReg(values.BPLCON0, "ECSENA", 0),
]);
this.referencedVariables.set(fields.BPLCON1, [
// TODO: upper bytes
this.byteReg(values.BPLCON1, "PF2H", 7, 4),
this.byteReg(values.BPLCON1, "PF1H", 3, 0),
]);
this.referencedVariables.set(fields.BPLCON2, [
this.byteReg(values.BPLCON2, "ZDBPSEL0", 14, 12),
this.boolReg(values.BPLCON2, "ZDBPEN", 11),
this.boolReg(values.BPLCON2, "ZDCTEN", 10),
this.boolReg(values.BPLCON2, "KILLEHB", 9),
this.boolReg(values.BPLCON2, "RDRAM", 8),
this.boolReg(values.BPLCON2, "SOGEN", 7),
this.boolReg(values.BPLCON2, "PF2PRI", 6),
this.byteReg(values.BPLCON2, "PF2P", 4, 3),
this.byteReg(values.BPLCON2, "PF1P", 2, 0),
]);
this.referencedVariables.set(fields.BPLCON3, [
this.byteReg(values.BPLCON3, "BANK", 15, 13),
this.byteReg(values.BPLCON3, "PF2OF", 12, 10),
this.boolReg(values.BPLCON3, "LOCT", 9),
this.byteReg(values.BPLCON3, "SPRES", 7, 6),
this.boolReg(values.BPLCON3, "BRDRBLNK", 5),
this.boolReg(values.BPLCON3, "BRDNTRAN", 4),
this.boolReg(values.BPLCON3, "ZDCLKEN", 2),
this.boolReg(values.BPLCON3, "BRDSPRT", 1),
this.boolReg(values.BPLCON3, "EXTBLKEN", 0),
]);
this.referencedVariables.set(fields.BPLCON4, [
this.byteReg(values.BPLCON4, "BPLAM", 15, 8),
this.byteReg(values.BPLCON4, "ESPRM", 7, 4),
this.byteReg(values.BPLCON4, "OSPRM", 3, 0),
]);
this.referencedVariables.set(fields.CLXCON, [
this.boolReg(values.CLXCON, "ENSP7", 15),
this.boolReg(values.CLXCON, "ENSP5", 14),
this.boolReg(values.CLXCON, "ENSP3", 13),
this.boolReg(values.CLXCON, "ENSP1", 12),
this.boolReg(values.CLXCON, "ENBP6", 11),
this.boolReg(values.CLXCON, "ENBP5", 10),
this.boolReg(values.CLXCON, "ENBP4", 9),
this.boolReg(values.CLXCON, "ENBP3", 8),
this.boolReg(values.CLXCON, "ENBP2", 7),
this.boolReg(values.CLXCON, "ENBP1", 6),
this.boolReg(values.CLXCON, "MVBP6", 5),
this.boolReg(values.CLXCON, "MVBP5", 4),
this.boolReg(values.CLXCON, "MVBP4", 3),
this.boolReg(values.CLXCON, "MVBP3", 2),
this.boolReg(values.CLXCON, "MVBP2", 1),
this.boolReg(values.CLXCON, "MVBP1", 0),
]);
this.referencedVariables.set(fields.CLXCON2, [
this.boolReg(values.CLXCON2, "ENBP8", 7),
this.boolReg(values.CLXCON2, "ENBP7", 6),
this.boolReg(values.CLXCON2, "MVBP8", 1),
this.boolReg(values.CLXCON2, "MVBP7", 0),
]);
this.referencedVariables.set(fields.COPCON, [
this.boolReg(values.COPCON, "CDANG", 1),
]);
this.referencedVariables.set(fields.DDFSTRT, [
this.wordReg(values.DDFSTRT, "H", 8, 0),
]);
this.referencedVariables.set(fields.DDFSTRT, [
this.wordReg(values.DDFSTOP, "H", 8, 0),
]);
this.referencedVariables.set(fields.DIWSTRT, [
this.byteReg(values.DIWSTRT, "V", 15, 8),
this.byteReg(values.DIWSTRT, "H", 7, 0), // << 2
]);
this.referencedVariables.set(fields.DIWSTOP, [
this.byteReg(values.DIWSTOP, "V", 15, 8),
this.byteReg(values.DIWSTOP, "H", 7, 0), // << 2
]);
this.referencedVariables.set(fields.DMACON, [
this.boolReg(values.DMACON, "BBUSY", 14),
this.boolReg(values.DMACON, "BZERO", 13),
this.boolReg(values.DMACON, "BLTPRI", 10),
this.boolReg(values.DMACON, "DMAEN", 9),
this.boolReg(values.DMACON, "BPLEN", 8),
this.boolReg(values.DMACON, "COPEN", 7),
this.boolReg(values.DMACON, "BLTEN", 6),
this.boolReg(values.DMACON, "SPREN", 5),
this.boolReg(values.DMACON, "DSKEN", 4),
this.boolReg(values.DMACON, "AUD3EN", 3),
this.boolReg(values.DMACON, "AUD2EN", 2),
this.boolReg(values.DMACON, "AUD1EN", 1),
this.boolReg(values.DMACON, "AUD0EN", 0),
]);
this.referencedVariables.set(fields.INTENA, [
this.boolReg(values.INTENA, "INTEN", 14),
this.boolReg(values.INTENA, "EXTER", 13),
this.boolReg(values.INTENA, "DSKSYN", 12),
this.boolReg(values.INTENA, "RBF", 11),
this.boolReg(values.INTENA, "AUD3", 10),
this.boolReg(values.INTENA, "AUD2", 9),
this.boolReg(values.INTENA, "AUD1", 8),
this.boolReg(values.INTENA, "AUD0", 7),
this.boolReg(values.INTENA, "BLIT", 6),
this.boolReg(values.INTENA, "VERTB", 5),
this.boolReg(values.INTENA, "COPER", 4),
this.boolReg(values.INTENA, "PORTS", 3),
this.boolReg(values.INTENA, "SOFT", 2),
this.boolReg(values.INTENA, "DSKBLK", 1),
this.boolReg(values.INTENA, "TBE", 0),
]);
for (let i = 0; i < 8; i++) {
const pos = `SPR${i}POS`;
this.referencedVariables.set(fields[pos], [
this.byteReg(values[pos], "SV", 15, 8),
this.byteReg(values[pos], "SH", 7, 0), // << 1
]);
const ctl = `SPR${i}CTL`;
this.referencedVariables.set(fields[ctl], [
this.byteReg(values[ctl], "EV", 15, 8),
this.boolReg(values[ctl], "ATT", 7),
this.boolReg(values[ctl], "SV8", 2),
this.boolReg(values[ctl], "EV8", 1),
this.boolReg(values[ctl], "SH0", 0),
]);
}
// Convert values to variable objects
const variables = {};
for (let key in values) {
const address = hardware_1.customRegisterAddresses[key];
if (!address)
continue;
const memoryReference = address.toString(16);
let value;
// Add fields if defined
const variablesReference = fields[key] ?? 0;
const isHigh = key.endsWith("H");
const isLow = key.endsWith("L");
const lowKey = key.replace(/H$/, "L");
const highKey = key.replace(/L$/, "H");
if (isHigh && values[lowKey]) {
// Combine high/low words into single longword value
const num = parseInt(values[key] + values[lowKey], 16);
key = key.replace(/H$/, "");
value = this.formatVariable(key, num, strings_1.NumberFormat.HEXADECIMAL, 4);
}
else if (!(isLow && values[highKey])) {
// Ignore keys for low register which will have been combined
const num = parseInt(values[key], 16);
const format = key.match(/[FL]WM$/)
? strings_1.NumberFormat.BINARY // Binary for masks
: strings_1.NumberFormat.HEXADECIMAL;
value = this.formatVariable(key, num, format, 2);
}
if (value) {
variables[key] = {
name: key,
value,
type: variablesReference ? "array" : "register",
variablesReference,
memoryReference,
};
}
}
// Simple registers with no nesting
const singleRegs = Object.keys(variables)
.filter((key) => !key.match(/^((AUD|BPL|BPLCON|COLOR|SPR)[0-9]|BLT[A-D])/))
.map((name) => variables[name]);
// Group numbered registers/sets as arrays with their own variablesReference:
// Get all variables starting with a prefix and unprefix the name property
const getPrefixed = (prefix, replace = prefix) => Object.keys(variables)
.filter((key) => key.match(prefix))
.map((name) => ({
...variables[name],
name: name.replace(replace, ""),
}));
// COLORXX
const colors = getPrefixed(/^COLOR\d/, "COLOR");
const colorsScope = this.scopes.create({ type: ScopeType.Custom, frameId });
this.referencedVariables.set(colorsScope, colors);
// BPLCONX
const bplCons = getPrefixed(/^BPLCON\d/, "BPLCON");
const bplConScope = this.scopes.create({ type: ScopeType.Custom, frameId });
this.referencedVariables.set(bplConScope, bplCons);
// BLTX
const blt = [];
for (const i of ["A", "B", "C", "D"]) {
const vars = getPrefixed(new RegExp("BLT" + i));
const scope = this.scopes.create({ type: ScopeType.Custom, frameId });
this.referencedVariables.set(scope, vars);
blt.push({
name: i,
type: "array",
value: "Channel " + i,
variablesReference: scope,
});
}
const bltScope = this.scopes.create({ type: ScopeType.Custom, frameId });
this.referencedVariables.set(bltScope, blt);
// AUDX
const aud = [];
for (let i = 0; i < 4; i++) {
const vars = getPrefixed(new RegExp("AUD" + i));
const scope = this.scopes.create({ type: ScopeType.Custom, frameId });
this.referencedVariables.set(scope, vars);
aud.push({
name: i.toString(),
type: "array",
value: "Channel " + i,
variablesReference: scope,
});
}
const audScope = this.scopes.create({ type: ScopeType.Custom, frameId });
this.referencedVariables.set(audScope, aud);
// BPLX
const bpl = [];
for (let i = 1; i < 9; i++) {
const vars = getPrefixed(new RegExp("BPL" + i));
const scope = this.scopes.create({ type: ScopeType.Custom, frameId });
this.referencedVariables.set(scope, vars);
bpl.push({
name: i.toString(),
type: "array",
value: "Bitplane " + i,
variablesReference: scope,
});
}
const bplScope = this.scopes.create({ type: ScopeType.Custom, frameId });
this.referencedVariables.set(bplScope, bpl);
// SPRX
const spr = [];
for (let i = 0; i < 8; i++) {
const vars = getPrefixed(new RegExp("SPR" + i));
const scope = this.scopes.create({ type: ScopeType.Custom, frameId });
this.referencedVariables.set(scope, vars);
spr.push({
name: i.toString(),
type: "array",
value: "Sprite " + i,
variablesReference: scope,
});
}
const sprScope = this.scopes.create({ type: ScopeType.Custom, frameId });
this.referencedVariables.set(sprScope, spr);
return [
...singleRegs,
// Base variables for groups
{
name: "COLORXX",
type: "array",
value: "Colors",
variablesReference: colorsScope,
},
{
name: "AUDX",
type: "array",
value: "Audio channels",
variablesReference: audScope,
},
{
name: "BPLCONX",
type: "array",
value: "Bitplane control",
variablesReference: bplConScope,
},
{
name: "BLTX",
type: "array",
value: "Blitter channels",
variablesReference: bltScope,
},
{
name: "BPLX",
type: "array",
value: "Bitplanes",
variablesReference: bplScope,
},
{
name: "SPRX",
type: "array",
value: "Sprites",
variablesReference: sprScope,
},
].sort((a, b) => (a.name > b.name ? 1 : -1));
}
/**
* Set the value of a variable
*
* Only registers are supported.
*/
async setVariable(variablesReference, name, value) {
const scopeRef = this.scopes.get(variablesReference);
let numValue = await this.evaluate(value);
if (typeof numValue !== "number") {
throw new Error("Value is not numeric");
}
switch (scopeRef?.type) {
case ScopeType.Registers:
if (numValue < 0) {
numValue += 0x100000000;
}
if (Math.abs(numValue) > 0x100000000) {
throw new Error("Register value out of range");
}
await this.gdb.setRegister((0, registers_1.getRegisterIndex)(name), numValue);
return this.formatVariable(name, numValue, strings_1.NumberFormat.HEXADECIMAL, 4);
case ScopeType.Vectors: {
const [addr] = name.split(" ");
await this.gdb.writeMemory(parseInt(addr, 16), numValue.toString(16).padStart(8, "0"));
return this.formatVariable(name, numValue, strings_1.NumberFormat.HEXADECIMAL, 4);
}
case ScopeType.Custom: {
// Find address of register:
// Check global register list. May need a a suffix for high word if combined.
let address = hardware_1.customRegisterAddresses[name] || hardware_1.customRegisterAddresses[name + "H"];
// Check fields in scope for memoryReference
if (!address) {
const v = this.referencedVariables
.get(variablesReference)
?.find((n) => n.name === name);
if (v?.memoryReference) {
address = parseInt(v.memoryReference, 16);
}
else {
throw new Error("Address not found");
}
}
// Check size to write - longword for combined pointer addresses
const isLong = name.endsWith("PT") || name.endsWith("LC");
const size = isLong ? 8 : 4;
await this.gdb.writeMemory(address, numValue.toString(16).padStart(size, "0"));
return this.formatVariable(name, numValue, strings_1.NumberFormat.HEXADECIMAL, size);
}
default:
throw new Error("This variable cannot be set");
}
}
// Variable formatting:
/**
* Format a variable as a string in the preferred NumberFormat
*/
formatVariable(variableName,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value, defaultFormat = strings_1.NumberFormat.HEXADECIMAL, minBytes = 0) {
if (typeof value !== "number") {
return String(value);
}
const format = this.variableFormatterMap.get(variableName) ?? defaultFormat;
return (0, strings_1.formatNumber)(value, format, minBytes);
}
/**
* Set the preferred format for a specific variable
*/
setVariableFormat(variableName, format) {
this.variableFormatterMap.set(variableName, format);
}
// Expressions:
/**
* Evaluate an expression or custom command
*
* @returns Single value or array
*/
async evaluateExpression({ expression, frameId, context, }) {
let variables;
let result;
const { length, wordLength, rowLength } = this.getMemoryFormat(context);
// Find expression type:
const isRegister = expression.match(/^([ad][0-7]|pc|sr)$/i) !== null;
const symbols = this.sourceMap.getSymbols();
const isSymbol = symbols[expression] !== undefined;
const commandMatch = expression.match(/([mdcph?])(\s|$)/i);
const command = commandMatch?.[1];
switch (command) {
case "m":
variables = await this.dumpMemoryCommand(expression, frameId);
break;
case "M":
variables = await this.writeMemoryCommand(expression, frameId);
break;
case "d":
variables = await this.disassembleCommand(expression, frameId);
break;
case "c":
variables = await this.disassembleCopperCommand(expression, frameId);
break;
case "h":
case "H":
case "?":
return;
default:
if (isRegister) {
await this.gdb.withFrame(frameId, async () => {
const address = await this.gdb.getRegister((0, registers_1.getRegisterIndex)(expression));
if (expression.startsWith("a") && context === "watch") {
variables = await this.readMemoryAsVariables(address, length, wordLength, rowLength);
}
else {
result = this.formatVariable(expression, address, strings_1.NumberFormat.HEXADECIMAL, 4);
}
});
}
else if (isSymbol && (context === "watch" || context === "hover")) {
const address = symbols[expression];
variables = await this.readMemoryAsVariables(address, length, wordLength, rowLength);
}
else {
// Evaluate
const value = await this.evaluate(expression, frameId);
result = this.formatVariable(expression, value, strings_1.NumberFormat.HEXADECIMAL);
}
}
// Build response for either single value or array
if (result) {
return {
result,
type: "string",
variablesReference: 0,
};
}
if (variables) {
const variablesReference = this.scopes.create({
type: ScopeType.Expression,
frameId: frameId ?? 0,
});
this.referencedVariables.set(variablesReference, variables);
return {
result: variables[0].value.replace(/^[0-9a-f]{2} [0-9a-f]{2} [0-9a-f]{2} [0-9a-f]{2}\s+/, ""),
type: "array",
variablesReference,
};
}
throw new Error("No result");
}
getMemoryFormat(context) {
const defaultFormat = {
length: 24,
wordLength: 2,
};
return context
? this.memoryFormats[context] ?? defaultFormat
: defaultFormat;
}
/**
* Evaluate simple expression to numeric value
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async evaluate(expression, frameIndex) {
// Convert all numbers to decimal:
let exp = expression
// Hex
.replace(/(\$|0x)([0-9a-f]+)/gi, (_, _2, d) => parseInt(d, 16).toString())
// Octal
.replace(/(@|0o)([0-7]+)/gi, (_, _2, d) => parseInt(d, 8).toString())
// Binary
.replace(/(%|0b)([0-1]+)/gi, (_, _2, d) => parseInt(d, 2).toString());
// Return value if numeric
if (exp.match(/^[0-9]+$/i)) {
return parseInt(exp, 10);
}
// Add prefix when referencing register fields
exp = exp.replace(/([ad][0-7]+)\./i, "__OBJ__$1.");
// ${expression} is replaced with expressio for backwards compatiblity
exp = exp.replace(/\$\{([^}]+)\}/, "$1");
const variables = await this.getVariables(frameIndex);
// Memory references:
// Numeric value at memory address
// Legacy syntax:
// #{expression}
const legacyMemMatches = exp.matchAll(/#\{(?<address>[^},]+)\}/gi);
for (const match of legacyMemMatches) {
const { address } = match.groups ?? {};
const addressNum = await this.evaluate(address, frameIndex);
if (typeof addressNum !== "number") {
throw new Error("address is not numeric");
}
const value = await this.getMemory(addressNum);
exp = exp.replace(match[0], value.toString());
}
// @(expression[,size=4])
// @s(expression[,size=4]) - signed
const memMatches = exp.matchAll(/@(?<sign>[su])?\((?<address>[^),]+)(,\s*(?<length>\d))?\)/gi);
for (const match of memMatches) {
const { address, length, sign } = match.groups ?? {};
const addressNum = await this.evaluate(address, frameIndex);
if (typeof addressNum !== "number") {
throw new Error("address is not numeric");
}
const lengthNum = length ? parseInt(length) : 4;
let value = await this.getMemory(addressNum, lengthNum);
if (sign === "s" || sign === "S") {
const range = Math.pow(2, lengthNum * 8);
if (value >= range / 2) {
value -= range;
}
}
exp = exp.replace(match[0], value.toString());
}
// Evaluate expression
return (0, expression_eval_1.eval)((0, expression_eval_1.parse)(exp), variables);
}
async writeMemoryCommand(expression, frameId) {
const matches = /M\s*(?<addr>[{}$#0-9a-z_]+)\s*=\s*(?<data>[0-9a-z_]+)/i.exec(expression);
const groups = matches?.groups;
if (!groups) {
throw new Error("Expected syntax: M address=bytes");
}
const address = await this.evaluate(groups.addr, frameId);
if (typeof address !== "number") {
throw new Error("address is not numeric");
}
await this.gdb.writeMemory(address, groups.data);
return this.readMemoryAsVariables(address, groups.data.length / 2);
}
async dumpMemoryCommand(expression, frameId) {
// Parse expression
const matches = /m\s*(?<address>[^,]+)(,\s*(?<length>(?!(d|c|ab?|ba?)$)[^,]+))?(,\s*(?<wordLength>(?!(d|c|ab?|ba?)$)[^,]+))?(,\s*(?<rowLength>(?!(d|c|ab?|ba?)$)[^,]+))?(,\s*(?<mode>(d|c|ab?|ba?)))?/i.exec(expression);
const groups = matches?.groups;
if (!groups) {
throw new Error("Expected syntax: m address[,size=16,wordSizeInBytes=4,rowSizeInWords=4][,ab]");
}
// Evaluate match groups:
// All of these parameters can contain expressions
const address = await this.evaluate(groups.address, frameId);
if (typeof address !== "number") {
throw new Error("address is not numeric");
}
const length = groups.length
? await this.evaluate(groups.length, frameId)
: 16;
if (typeof length !== "number") {
throw new Error("length is not numeric");
}
const wordLength = groups.wordLength
? await this.evaluate(groups.wordLength, frameId)
: 4;
if (typeof wordLength !== "number") {
throw new Error("wordLength is not numeric");
}
const rowLength = groups.rowLength
? await this.evaluate(groups.rowLength, frameId)
: 4;
if (typeof rowLength !== "number") {
throw new Error("rowLength is not numeric");
}
const mode = groups.mode ?? "ab";
if (mode === "d") {
return await this.disassembleAsVariables(address, length);
}
else if (mode === "c") {
return await this.disassembleCopperAsVariables(address, length);
}
else {
return await this.readMemoryAsVariables(address, length, wordLength, rowLength, mode);
}
}
async disassembleCommand(expression, frameId) {
const matches = /d\s*(?<address>[^,]+)(,\s*(?<length>[^,]+))?/i.exec(expression);
const groups = matches?.groups;
if (!groups) {
throw new Error("Expected syntax: d address[,size=16]");
}
const address = await this.evaluate(groups.address, frameId);
if (typeof address !== "number") {
throw new Error("address is not numeric");
}
const length = groups.length
? await this.evaluate(groups.length, frameId)
: 16;
if (typeof length !== "number") {
throw new Error("length is not numeric");
}
return this.disassembleAsVariables(address, length);
}
async disassembleCopperCommand(expression, frameId) {
const matches = /c\s*(?<address>[^,]+)(,\s*(?<length>[^,]+))?/i.exec(expression);
const groups = matches?.groups;
if (!groups) {
throw new Error("Expected syntax: c address[,size=16]");
}
const address = await this.evaluate(groups.address, frameId);
if (typeof address !== "number") {
throw new Error("address is not numeric");
}
const length = groups.length
? await this.evaluate(groups.length, frameId)
: 16;
if (typeof length !== "number") {
throw new Error("length is not numeric");
}
return this.disassembleCopperAsVariables(address, length);
}
async readMemoryAsVariables(address, length = 16, wordLength = 4, rowLength = 4, mode = "ab") {
const memory = await this.gdb.readMemory(address, length);
let firstRow = "";
const variables = new Array();
const chunks = (0, strings_1.chunk)(memory.toString(), wordLength * 2);
let i = 0;
let rowCount = 0;
let row = "";
let nextAddress = address;
let lineAddress = address;
while (i < chunks.length) {
if (rowCount > 0) {
row += " ";
}
row += chunks[i];
nextAddress += chunks[i].length / 2;
if (rowCount >= rowLength - 1 || i === chunks.length - 1) {
if (mode.indexOf("a") >= 0) {
const asciiText = (0, strings_1.hexStringToASCII)(row.replace(/\s+/g, ""), 2);
if (mode.indexOf("b") >= 0) {
if (i === chunks.length - 1 && rowCount < rowLength - 1) {
const chunksMissing = rowLength - 1 - rowCount;
const padding = chunksMissing * wordLength * 2 + chunksMissing;
for (let j = 0; j < padding; j++) {
row += " ";
}
}
row += " | ";
}
else {
row = "";
}
row += asciiText;
}
variables.push({
value: row,
name: lineAddress.toString(16).padStart(8, "0"),
variablesReference: 0,
});
if (firstRow.length <= 0) {
firstRow = row;
}
rowCount = 0;
lineAddress = nextAddress;
row = "";
}
else {
rowCount++;
}
i++;
}
return variables;
}
async disassembleAsVariables(address, length) {
const memory = await this.gdb.readMemory(address, length);
const { instructions } = await (0, disassembly_1.disassemble)(memory, address);
return instructions.map(({ instruction, address, instructionBytes }) => ({
value: (instructionBytes ?? "").padEnd(26) + instruction,
name: address,
variablesReference: 0,
}));
}
async disassembleCopperAsVariables(address, length) {
const memory = await this.gdb.readMemory(address, length);
return (0, disassembly_1.disassembleCopper)(memory).map((inst, i) => ({
value: inst.toString(),
name: (0, strings_1.formatAddress)(address + i * 4),
variablesReference: 0,
}));
}
// Location utils
// Helpers to build variables from byte ranges:
boolReg(value, name, bit) {
return {
name,
type: "register",
value: (0, strings_1.bitValue)(parseInt(value, 16), bit) ? "1" : "0",
variablesReference: 0,
};
}
byteReg(value, name, hi, lo) {
return {
name,
type: "register",
value: this.formatVariable(name, (0, strings_1.bitValue)(parseInt(value, 16), hi, lo), strings_1.NumberFormat.HEXADECIMAL, 1),
variablesReference: 0,
};
}
wordReg(value, name, hi, lo) {
return {
name,
type: "register",
value: this.formatVariable(name, (0, strings_1.bitValue)(parseInt(value, 16), hi, lo), strings_1.NumberFormat.HEXADECIMAL, 2),
variablesReference: 0,
};
}
/**
* Get symbol name and offset for address
*/
symbolOffset(address) {