1443 lines
44 KiB
JavaScript
1443 lines
44 KiB
JavaScript
import { Disassembler } from /**/"../core/Disassembler.js";
|
|
import { Toolkit } from /**/"../toolkit/Toolkit.js";
|
|
let register = Debugger => {
|
|
|
|
// CPU disassembler and register state window
|
|
class CPU extends Toolkit.Window {
|
|
|
|
///////////////////////// Initialization Methods //////////////////////////
|
|
|
|
constructor(debug, index) {
|
|
super(debug.app, {
|
|
class: "tk window cpu"
|
|
});
|
|
|
|
// Configure instance fields
|
|
this.debug = debug;
|
|
this.height = 300;
|
|
this.index = index;
|
|
this.shown = false;
|
|
this.width = 400;
|
|
|
|
// Window
|
|
this.setTitle("{debug.cpu._}", true);
|
|
this.substitute("#", " " + (index + 1));
|
|
if (index == 1)
|
|
this.element.classList.add("two");
|
|
this.addEventListener("close" , e=>this.visible = false);
|
|
this.addEventListener("visibility", e=>this.onVisibility(e));
|
|
|
|
// Client area
|
|
Object.assign(this.client.style, {
|
|
display : "grid",
|
|
gridAutoRows : "100%",
|
|
gridTemplateColumns: "auto"
|
|
});
|
|
|
|
// Disassembler
|
|
this.disassembler = new DisassemblerPane(this);
|
|
this.lastFocus = this.disassembler.view;
|
|
|
|
// Register lists
|
|
this.registers = new RegisterPane(this);
|
|
|
|
// Main content area
|
|
this.add(new Toolkit.SplitPane(debug.app, {
|
|
orientation: "right",
|
|
primary : this.registers,
|
|
secondary : this.disassembler
|
|
}));
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Event Handlers //////////////////////////////
|
|
|
|
// Window key press
|
|
onKeyDown(e) {
|
|
super.onKeyDown(e);
|
|
|
|
// Error checking
|
|
if (e.altKey || e.shiftKey)
|
|
return;
|
|
|
|
// Processing by key: CTRL down
|
|
if (e.ctrlKey) switch (e.key) {
|
|
case "b": case "B":
|
|
this.disassembler.bytesColumn = !this.disassembler.bytesColumn;
|
|
break;
|
|
case "f": case "F":
|
|
this.disassembler.fitColumns();
|
|
break;
|
|
case "g": case "G":
|
|
this.disassembler.goto();
|
|
return;
|
|
}
|
|
|
|
// Processing by key: CTRL up
|
|
else switch (e.key) {
|
|
case "F10":
|
|
this.debug.app.runNext(this.index, { refresh: true });
|
|
break;
|
|
case "F11":
|
|
this.debug.app.singleStep(this.index, { refresh: true });
|
|
break;
|
|
default: return;
|
|
}
|
|
|
|
Toolkit.handle(e);
|
|
}
|
|
|
|
// Window visibility
|
|
onVisibility(e) {
|
|
let firstShow = !this.shown && e.visible;
|
|
|
|
// Configure instance fields
|
|
this.shown = this.shown || e.visible;
|
|
|
|
// Window visible for the first time
|
|
if (firstShow) {
|
|
this.disassembler.firstShow();
|
|
this.registers .firstShow();
|
|
}
|
|
|
|
// Configure subscriptions
|
|
if (!e.visible) {
|
|
if (this.registers)
|
|
this.registers .unsubscribe();
|
|
if (this.disassembler)
|
|
this.disassembler.unsubscribe();
|
|
} else {
|
|
this.registers .fetch();
|
|
this.disassembler.fetch();
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
class DisassemblerPane extends Toolkit.ScrollPane {
|
|
|
|
///////////////////////// Initialization Methods //////////////////////////
|
|
|
|
constructor(cpu) {
|
|
super(cpu.debug.app, {
|
|
class : "tk scroll-pane scr-dasm",
|
|
overflowX: "auto",
|
|
overflowY: "hidden"
|
|
});
|
|
|
|
// Configure instance fields
|
|
this._bytesColumn = true;
|
|
this.columnWidths = [ 0, 0, 0, 0, 0, 0 ];
|
|
this.cpu = cpu;
|
|
this.delta = 0;
|
|
this.dasm = null;
|
|
this.lines = [];
|
|
this.pending = false;
|
|
this.subscription = [ 1, cpu.index, "cpu", "disassembler", "refresh" ],
|
|
this.viewAddress = 0xFFFFFFF0;
|
|
this.viewLine = 10;
|
|
|
|
// Client area
|
|
let view = this.view = new Toolkit.Component(cpu.debug.app, {
|
|
class : "tk mono disassembler",
|
|
role : "application",
|
|
tabIndex: "0",
|
|
style : {
|
|
display : "grid",
|
|
height : "100%",
|
|
minWidth: "100%",
|
|
overflow: "hidden",
|
|
position: "relative",
|
|
width : "max-content"
|
|
}
|
|
});
|
|
view.setLabel("{debug.cpu.disassembler}", true);
|
|
view.setRoleDescription("{debug.cpu.disassembler}", true);
|
|
view.addEventListener("keydown", e=>this.viewKeyDown(e));
|
|
view.addEventListener("resize" , e=>this.viewResize ( ));
|
|
view.addEventListener("wheel" , e=>this.viewWheel (e));
|
|
|
|
// Label for measuring text dimensions in the disassembler
|
|
this.sizer = new Toolkit.Label(cpu.debug.app, {
|
|
class : "tk label mono",
|
|
visible : false,
|
|
visibility: true,
|
|
style: {
|
|
position: "absolute"
|
|
}
|
|
});
|
|
this.sizer.setText("\u00a0", false); //
|
|
view.append(this.sizer);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Event Handlers //////////////////////////////
|
|
|
|
// Column resized
|
|
colResize() {
|
|
if (this.lines.length == 0)
|
|
return;
|
|
for (let x = 0; x < this.columnWidths.length; x++) {
|
|
let elm = this.lines[0].all[x];
|
|
let width = elm.getBoundingClientRect().width;
|
|
if (width <= this.columnWidths[x])
|
|
continue;
|
|
this.columnWidths[x] = width;
|
|
elm.style.minWidth = width + "px";
|
|
}
|
|
}
|
|
|
|
// Key press
|
|
viewKeyDown(e) {
|
|
|
|
// Error checking
|
|
if (e.altKey || e.ctrlKey || e.shiftKey)
|
|
return;
|
|
|
|
// Processing by key
|
|
switch (e.key) {
|
|
case "ArrowDown":
|
|
this.fetch(-1);
|
|
break;
|
|
case "ArrowLeft":
|
|
this.scrollLeft -= this.hscroll.unitIncrement;
|
|
break;
|
|
case "ArrowRight":
|
|
this.scrollLeft += this.hscroll.unitIncrement;
|
|
break;
|
|
case "ArrowUp":
|
|
this.fetch(+1);
|
|
break;
|
|
case "PageDown":
|
|
this.fetch(-this.tall(true));
|
|
break;
|
|
case "PageUp":
|
|
this.fetch(+this.tall(true));
|
|
break;
|
|
default: return;
|
|
}
|
|
|
|
Toolkit.handle(e);
|
|
}
|
|
|
|
// Resize
|
|
viewResize() {
|
|
|
|
// Error checking
|
|
if (!this.sizer)
|
|
return;
|
|
|
|
// Working variables
|
|
let tall = this.tall(false) + 1;
|
|
let grew = this.lines.length < tall;
|
|
|
|
// Process all new lines
|
|
for (let y = this.lines.length; y < tall; y++) {
|
|
let first = y == 0 ? " first" : "";
|
|
let resizer = y != 0 ? null :
|
|
new ResizeObserver(()=>this.colResize());
|
|
|
|
let line = {
|
|
lblAddress : document.createElement("div"),
|
|
lblBytes : [],
|
|
lblMnemonic: document.createElement("div"),
|
|
lblOperands: document.createElement("div"),
|
|
spacer : document.createElement("div")
|
|
};
|
|
|
|
// Address label
|
|
line.lblAddress.className = "addr" + first;
|
|
if (y == 0)
|
|
resizer.observe(line.lblAddress);
|
|
this.view.append(line.lblAddress);
|
|
|
|
// Byte labels
|
|
for (let x = 0; x < 4; x++) {
|
|
let lbl = line.lblBytes[x] = document.createElement("div");
|
|
lbl.className = "byte" + first + (x == 0 ? " b0" : "");
|
|
if (y == 0) {
|
|
lbl.style.minWidth = "0px";
|
|
resizer.observe(lbl);
|
|
}
|
|
this.view.append(lbl);
|
|
}
|
|
|
|
// Mnemonic label
|
|
line.lblMnemonic.className = "inst" + first;
|
|
if (y == 0)
|
|
resizer.observe(line.lblMnemonic);
|
|
this.view.append(line.lblMnemonic);
|
|
|
|
// Operand label
|
|
line.lblOperands.className = "ops" + first;
|
|
this.view.append(line.lblOperands);
|
|
|
|
// Growing spacer
|
|
line.spacer.className = "spacer" + first;
|
|
this.view.append(line.spacer);
|
|
|
|
// All elements
|
|
line.all = line.lblBytes.concat([
|
|
line.lblAddress,
|
|
line.lblMnemonic,
|
|
line.lblOperands,
|
|
line.spacer
|
|
]);
|
|
|
|
this.lines.push(line);
|
|
}
|
|
|
|
// Remove lines that are no longer visible
|
|
while (tall < this.lines.length) {
|
|
let line = this.lines[tall];
|
|
line.lblAddress .remove();
|
|
line.lblMnemonic.remove();
|
|
line.lblOperands.remove();
|
|
line.spacer .remove();
|
|
for (let lbl of line.lblBytes)
|
|
lbl.remove();
|
|
this.lines.splice(tall, 1);
|
|
}
|
|
|
|
// Configure scroll bar
|
|
this.hscroll.unitIncrement =
|
|
this.sizer.element.getBoundingClientRect().height;
|
|
|
|
// Update components
|
|
if (grew)
|
|
this.fetch();
|
|
else this.refresh();
|
|
}
|
|
|
|
// Mouse wheel
|
|
viewWheel(e) {
|
|
|
|
// Error checking
|
|
if (e.altKey || e.ctrlKey || e.shiftKey)
|
|
return;
|
|
|
|
// Always handle the event
|
|
Toolkit.handle(e);
|
|
|
|
// Determine how many full lines were scrolled
|
|
let scr = Debugger.linesScrolled(e,
|
|
this.sizer.element.getBoundingClientRect().height,
|
|
this.tall(true),
|
|
this.delta
|
|
);
|
|
this.delta = scr.delta;
|
|
scr.lines = Math.max(-3, Math.min(3, scr.lines));
|
|
|
|
// No lines were scrolled
|
|
if (scr.lines == 0)
|
|
return;
|
|
|
|
// Scroll the view
|
|
this.fetch(-scr.lines);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Package Methods /////////////////////////////
|
|
|
|
// Display the bytes column in the disassembler
|
|
get bytesColumn() { return this._bytesColumn; }
|
|
set bytesColumn(show) {
|
|
show = !!show;
|
|
if (show == this._bytesColumn)
|
|
return;
|
|
this._bytesColumn = show;
|
|
this.refresh();
|
|
}
|
|
|
|
// Retrieve a disassembly from the simulation state
|
|
async fetch(viewScroll = 0) {
|
|
|
|
// Select the parameters for the simulation fetch
|
|
let params = {
|
|
viewAddress: this.viewAddress,
|
|
viewLine : this.viewLine + viewScroll,
|
|
viewLength : this.tall(false) + 20,
|
|
viewScroll : viewScroll
|
|
};
|
|
if (this.pending instanceof Object) {
|
|
params.viewLine += this.pending.viewScroll;
|
|
params.viewScroll += this.pending.viewScroll;
|
|
}
|
|
let bounds = Disassembler.dataBounds(
|
|
params.viewAddress, params.viewLine, params.viewLength);
|
|
params.dataAddress = bounds.address;
|
|
params.dataLength = bounds.length;
|
|
|
|
// A communication with the core thread is already underway
|
|
if (this.pending) {
|
|
this.pending = params;
|
|
this.refresh();
|
|
return;
|
|
}
|
|
|
|
// Retrieve data from the simulation state
|
|
this.pending = params;
|
|
for (let data=null, promise=null; this.pending instanceof Object;) {
|
|
|
|
// Wait for a transaction to complete
|
|
if (promise != null) {
|
|
this.pending = true;
|
|
data = await promise;
|
|
promise = null;
|
|
}
|
|
|
|
// Initiate a new transaction
|
|
if (this.pending instanceof Object) {
|
|
params = this.pending;
|
|
let options = { tag: params };
|
|
if (this.cpu.isVisible())
|
|
options.subscription = this.subscription;
|
|
promise = this.cpu.debug.core.read(this.cpu.debug.sim,
|
|
params.dataAddress, params.dataLength, options);
|
|
}
|
|
|
|
// Process the result of a transaction
|
|
if (data != null) {
|
|
this.refresh(data);
|
|
data = null;
|
|
}
|
|
|
|
};
|
|
this.pending = false;
|
|
}
|
|
|
|
// Component is being displayed for the first time
|
|
firstShow() {
|
|
this.viewLine = Math.floor(this.tall(true) / 3) + 10;
|
|
}
|
|
|
|
// Shrink all columns to fit the current view
|
|
fitColumns() {
|
|
if (this.lines.length == 0)
|
|
return;
|
|
for (let elm of this.lines[0].all)
|
|
elm.style.removeProperty("min-width");
|
|
for (let x = 0; x < this.columnWidths.length; x++)
|
|
this.columnWidths[x] = 0;
|
|
this.refresh();
|
|
}
|
|
|
|
// Ensure PC is visible in the view
|
|
followPC(pc = null) {
|
|
let tall = this.tall(false);
|
|
let count = !this.dasm ? 0 : Math.min(this.dasm.length - 10, tall);
|
|
|
|
// Determine whether PC already is visible
|
|
for (let x = 0; x < count; x++) {
|
|
let line = this.dasm[x + 10];
|
|
if (pc - line.rawAddress >>> 0 < line.bytes.length)
|
|
return; // PC is already visible
|
|
}
|
|
|
|
// Request a new view containing PC
|
|
this.viewAddress = pc;
|
|
this.viewLine = Math.floor(tall / 3) + 10;
|
|
if (this.cpu.isVisible())
|
|
this.fetch();
|
|
}
|
|
|
|
// Prompt the user to navigate to a new editing address
|
|
goto() {
|
|
|
|
// Retrieve the value from the user
|
|
let addr = prompt(this.app.localize("{debug.cpu.goto}"));
|
|
if (addr === null)
|
|
return;
|
|
addr = parseInt(addr.trim(), 16);
|
|
if (
|
|
!Number.isInteger(addr) ||
|
|
addr < 0 ||
|
|
addr > 4294967295
|
|
) return;
|
|
|
|
// Navigate to the given address
|
|
this.viewAddress = addr;
|
|
this.viewLine = Math.floor(this.tall(true) / 3) + 10;
|
|
this.fetch();
|
|
}
|
|
|
|
// Update localization strings
|
|
localize() {
|
|
this.localizeRoleDescription();
|
|
this.localizeLabel();
|
|
}
|
|
|
|
// Update disassembler
|
|
refresh(msg = null) {
|
|
let tall = this.tall(false);
|
|
|
|
// Receiving data from the simulation state
|
|
if (msg != null) {
|
|
|
|
// Disassemble the retrieved data
|
|
this.dasm = this.cpu.debug.dasm.disassemble(
|
|
msg.data, msg.address,
|
|
msg.tag.viewAddress, msg.tag.viewLine, msg.tag.viewLength,
|
|
this.cpu.registers.pc
|
|
);
|
|
|
|
// Configure the view
|
|
this.viewAddress = this.dasm[10].rawAddress;
|
|
this.viewLine = 10;
|
|
if (this.pending instanceof Object)
|
|
this.viewLine += this.pending.viewScroll;
|
|
}
|
|
|
|
// Determine an initial number of visible byte columns
|
|
let showBytes = 0;
|
|
if (this.bytesColumn)
|
|
for (let x = 0; x<4 && this.columnWidths[x]!=0; x++, showBytes++);
|
|
|
|
// Process all lines
|
|
let index = 20 - this.viewLine;
|
|
for (let y = 0; y < this.lines.length; y++, index++) {
|
|
let line = this.lines[y];
|
|
let method = "remove";
|
|
|
|
// There is no data for this line
|
|
if (index < 0 || this.dasm == null || index >= this.dasm.length) {
|
|
line.lblAddress .innerText = "--------";
|
|
line.lblBytes[0].innerText = "--";
|
|
line.lblMnemonic.innerText = "---";
|
|
line.lblOperands.innerText = "";
|
|
for (let x = 1; x < 4; x++)
|
|
line.lblBytes[x].innerText = "";
|
|
if (this.bytesColumn)
|
|
showBytes = Math.max(showBytes, 1);
|
|
}
|
|
|
|
// Present the disassembled line
|
|
else {
|
|
let dasm = this.dasm[index];
|
|
line.lblAddress .innerText = dasm.address;
|
|
line.lblMnemonic.innerText = dasm.mnemonic;
|
|
line.lblOperands.innerText = dasm.operands.join(", ");
|
|
for (let x = 0; x < 4; x++)
|
|
line.lblBytes[x].innerText = dasm.bytes[x] || "";
|
|
if (this.cpu.registers.pc == dasm.rawAddress)
|
|
method = "add";
|
|
if (this.bytesColumn)
|
|
showBytes = Math.max(showBytes, dasm.bytes.length);
|
|
}
|
|
|
|
// Configure whether PC is on this line
|
|
for (let elm of line.all)
|
|
elm.classList[method]("pc");
|
|
}
|
|
|
|
// Configure which byte columns are visible
|
|
for (let line of this.lines) {
|
|
for (let x = 0; x < 4; x++) {
|
|
line.lblBytes[x].style
|
|
[x < showBytes ? "removeProperty" : "setProperty"]
|
|
("display", "none")
|
|
;
|
|
}
|
|
}
|
|
|
|
// Configure layout
|
|
this.view.element.style.gridTemplateColumns =
|
|
"repeat(" + (showBytes + 3) + ", max-content) auto";
|
|
}
|
|
|
|
// Stop receiving updates from the simulation
|
|
unsubscribe() {
|
|
this.cpu.debug.core.unsubscribe(this.subscription, false);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Private Methods /////////////////////////////
|
|
|
|
// Measure the number of lines visible in the view
|
|
tall(fully = null) {
|
|
return Math.max(1, Math[fully===null ? "abs" : fully?"floor":"ceil"](
|
|
this.view .element.getBoundingClientRect().height /
|
|
this.sizer.element.getBoundingClientRect().height
|
|
));
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
// Entry in the register lists
|
|
class Register {
|
|
|
|
//////////////////////////////// Constants ////////////////////////////////
|
|
|
|
// Register types
|
|
static PROGRAM = 0;
|
|
static PLAIN = 1;
|
|
static CHCW = 2;
|
|
static ECR = 3;
|
|
static PSW = 4;
|
|
static PIR = 5;
|
|
static TKCW = 6;
|
|
|
|
// Program register formats
|
|
static HEX = 0;
|
|
static SIGNED = 1;
|
|
static UNSIGNED = 2;
|
|
static FLOAT = 3;
|
|
|
|
// Expansion controls by register type
|
|
static FIELDS = {
|
|
[this.CHCW]: [
|
|
[ "check", "ICE", 1 ]
|
|
],
|
|
[this.ECR]: [
|
|
[ "texth", "FECC", 16, 16 ],
|
|
[ "texth", "EICC", 0, 16 ]
|
|
],
|
|
[this.PIR]: [
|
|
[ "texth", "PT", 0, 16 ]
|
|
],
|
|
[this.PSW]: [
|
|
[ "check", "CY" , 3 ],
|
|
[ "check", "FRO", 9 ],
|
|
[ "check", "OV" , 2 ],
|
|
[ "check", "FIV", 8 ],
|
|
[ "check", "S" , 1 ],
|
|
[ "check", "FZD", 7 ],
|
|
[ "check", "Z" , 0 ],
|
|
[ "check", "FOV", 6 ],
|
|
[ "check", "NP" , 15 ],
|
|
[ "check", "FUD", 5 ],
|
|
[ "check", "EP" , 14 ],
|
|
[ "check", "FPR", 4 ],
|
|
[ "check", "ID" , 12 ],
|
|
[ "textd", "I" , 16, 4 ],
|
|
[ "check", "AE" , 13 ]
|
|
],
|
|
[this.TKCW]: [
|
|
[ "check", "FIT", 7 ],
|
|
[ "check", "FUT", 4 ],
|
|
[ "check", "FZT", 6 ],
|
|
[ "check", "FPT", 3 ],
|
|
[ "check", "FVT", 5 ],
|
|
[ "check", "OTM", 8 ],
|
|
[ "check", "RDI", 2 ],
|
|
[ "textd", "RD" , 0, 2 ]
|
|
]
|
|
};
|
|
|
|
|
|
|
|
///////////////////////// Initialization Methods //////////////////////////
|
|
|
|
constructor(registers, key, type) {
|
|
let app = registers.cpu.debug.app;
|
|
|
|
// Configure instance fields
|
|
this.controls = [];
|
|
this.dasm = registers.cpu.debug.app.dasm;
|
|
this.debug = registers.cpu.debug;
|
|
this.expansion = null;
|
|
this.registers = registers;
|
|
this.target = registers;
|
|
this.type = type;
|
|
|
|
// Resolve the register reference
|
|
key = key.slice();
|
|
while (key.length != 1)
|
|
this.target = this.target[key.shift()];
|
|
this.key = key[0];
|
|
|
|
// Main controls
|
|
this.main = new Toolkit.Component(app, {
|
|
class: "main",
|
|
style: {
|
|
alignItems : "center",
|
|
display : "grid",
|
|
gridTemplateColumns: "max-content auto"
|
|
}
|
|
});
|
|
|
|
// Expand/collapse button
|
|
this.btnExpand = new Toolkit.Component(app, {
|
|
class: "tk expand",
|
|
style: {
|
|
alignItems : "center",
|
|
display : "grid",
|
|
gridTemplateColumns: "max-content auto"
|
|
}
|
|
});
|
|
this.main.add(this.btnExpand);
|
|
|
|
// Expand/collapse icon
|
|
this.icon = new Toolkit.Component(app, { class: "icon" });
|
|
this.btnExpand.add(this.icon);
|
|
|
|
// Register name
|
|
this.label = new Toolkit.Label(app, {
|
|
class: "label",
|
|
id : Toolkit.id()
|
|
});
|
|
this.btnExpand.add(this.label);
|
|
|
|
// Value text box
|
|
this.txtValue = new Toolkit.TextBox(app, {
|
|
class : "tk text-box mono",
|
|
spellcheck: "false",
|
|
size : "1",
|
|
value : "00000000"
|
|
});
|
|
this.txtValue.setLabel(this.label);
|
|
this.txtValue.addEventListener("action" , e=>this.valAction (e));
|
|
this.txtValue.addEventListener("keydown", e=>this.valKeyDown(e));
|
|
this.main.add(this.txtValue);
|
|
|
|
// Expansion area
|
|
if (type == Register.PROGRAM)
|
|
this.initProgram(app);
|
|
else this.initSystem(app, Register.FIELDS[type]);
|
|
if (this.expansion != null) {
|
|
|
|
// Expand/collapse button
|
|
this.btnExpand.element.setAttribute("aria-expanded", "false" );
|
|
this.btnExpand.element.setAttribute("tabindex" , "0" );
|
|
this.btnExpand.element.setAttribute("role" , "button");
|
|
this.btnExpand.element
|
|
.setAttribute("aria-controls", this.expansion.element.id);
|
|
|
|
// Expansion area
|
|
this.btnExpand.addEventListener("keydown",
|
|
e=>this.expKeyDown (e));
|
|
this.btnExpand.addEventListener("pointerdown",
|
|
e=>this.expPointerDown(e));
|
|
this.expansion.visible = false;
|
|
}
|
|
|
|
// Update controls
|
|
this.refresh();
|
|
|
|
// PSW is initially expanded
|
|
if (type == Register.PSW && key == 5)
|
|
this.expanded = true;
|
|
|
|
// System registers after PSW are initially hidden
|
|
else if (key != "pc" && key != 5 && type != Register.PROGRAM) {
|
|
this.main.style.position = "absolute";
|
|
this.main.style.visibility = "hidden";
|
|
}
|
|
|
|
}
|
|
|
|
// Expansion controls for program registers
|
|
initProgram(app) {
|
|
|
|
// Expansion area
|
|
let exp = this.expansion = new Toolkit.Component(app, {
|
|
class: "expansion",
|
|
id : Toolkit.id(),
|
|
style: {
|
|
display : "inline-grid",
|
|
gridTemplateColumns: "max-content",
|
|
}
|
|
});
|
|
exp.localize = ()=>exp.localizeLabel();
|
|
exp.setLabel("{debug.cpu.format}", true);
|
|
|
|
// Radio group
|
|
let group = new Toolkit.RadioGroup(app);
|
|
this.format = Register.HEX;
|
|
|
|
// Hex radio button
|
|
let opt = new Toolkit.Radio(app, {
|
|
checked: true,
|
|
group : group
|
|
});
|
|
opt.setText("{debug.cpu.hex}", true);
|
|
opt.addEventListener("input", e=>this.onProgram(Register.HEX));
|
|
exp.add(opt);
|
|
|
|
// Signed radio button
|
|
opt = new Toolkit.Radio(app, { group: group });
|
|
opt.setText("{debug.cpu.signed}", true);
|
|
opt.addEventListener("input", e=>this.onProgram(Register.SIGNED));
|
|
exp.add(opt);
|
|
|
|
// Unsigned radio button
|
|
opt = new Toolkit.Radio(app, { group: group });
|
|
opt.setText("{debug.cpu.unsigned}", true);
|
|
opt.addEventListener("input", e=>this.onProgram(Register.UNSIGNED));
|
|
exp.add(opt);
|
|
|
|
// Float radio button
|
|
opt = new Toolkit.Radio(app, { group: group });
|
|
opt.setText("{debug.cpu.float}", true);
|
|
opt.addEventListener("input", e=>this.onProgram(Register.FLOAT));
|
|
exp.add(opt);
|
|
}
|
|
|
|
// Expansion controls for system registers
|
|
initSystem(app, fields) {
|
|
|
|
// No expansion area
|
|
if (!fields)
|
|
return;
|
|
|
|
// Expansion area
|
|
let exp = this.expansion = new Toolkit.Component(app, {
|
|
class: "expansion",
|
|
id : Toolkit.id(),
|
|
style: {
|
|
display : "inline-grid",
|
|
gridTemplateColumns: "max-content max-content",
|
|
}
|
|
});
|
|
|
|
// Process all controls
|
|
for (let field of fields) {
|
|
|
|
// Bit check box
|
|
if (field[0] == "check") {
|
|
let box = new Toolkit.Checkbox(app);
|
|
box.setText(field[1], false);
|
|
box.bit = field[2];
|
|
box.addEventListener("input", e=>this.onBit(e));
|
|
exp.append(box.element);
|
|
if (this.type == Register.PIR || this.type == Register.TKCW)
|
|
box.disabled = true;
|
|
this.controls.push(box);
|
|
}
|
|
|
|
// Decimal text box
|
|
else if (field[0] == "textd") {
|
|
|
|
// Containing element for outer layout purposes
|
|
let div = document.createElement("div");
|
|
div.className = "text-dec";
|
|
Object.assign(div.style, {
|
|
alignItems : "center",
|
|
display : "inline-grid",
|
|
gridTemplateColumns: "max-content auto"
|
|
});
|
|
|
|
// Text box
|
|
let txt = new Toolkit.TextBox(app, {
|
|
id : Toolkit.id(),
|
|
spellcheck: false,
|
|
value : "0",
|
|
style : {
|
|
maxWidth: "2em"
|
|
}
|
|
});
|
|
txt.bit = field[2];
|
|
txt.bits = field[3];
|
|
txt.isHex = false;
|
|
txt.addEventListener("action", e=>this.onText(e));
|
|
|
|
// Label
|
|
let lbl = new Toolkit.Label(app, {
|
|
htmlFor: txt.element.id,
|
|
id : Toolkit.id(),
|
|
tag : "label"
|
|
});
|
|
lbl.setText(field[1], false);
|
|
|
|
txt.setLabel(lbl);
|
|
|
|
// Disable all fields
|
|
if (this.type == Register.PIR || this.type == Register.TKCW) {
|
|
lbl.disabled = true;
|
|
txt.disabled = true;
|
|
}
|
|
|
|
// Output control
|
|
this.controls.push(txt);
|
|
div.append(lbl.element);
|
|
div.append(txt.element);
|
|
exp.append(div);
|
|
}
|
|
|
|
// Hexadecimal text box
|
|
else if (field[0] == "texth") {
|
|
|
|
// Text box
|
|
let txt = new Toolkit.TextBox(app, {
|
|
class : "tk text-box mono",
|
|
id : Toolkit.id(),
|
|
spellcheck: false,
|
|
value : "0",
|
|
style : {
|
|
maxWidth: "3em"
|
|
}
|
|
});
|
|
txt.bit = field[2];
|
|
txt.bits = field[3];
|
|
txt.isHex = true;
|
|
txt.addEventListener("action", e=>this.onText(e));
|
|
|
|
// Label
|
|
let lbl = new Toolkit.Label(app, {
|
|
htmlFor: txt.element.id,
|
|
id : Toolkit.id(),
|
|
tag : "label"
|
|
});
|
|
lbl.setText(field[1], false);
|
|
|
|
txt.setLabel(lbl);
|
|
|
|
// Disable all fields
|
|
if (this.type == Register.PIR || this.type == Register.TKCW) {
|
|
lbl.disabled = true;
|
|
txt.disabled = true;
|
|
}
|
|
|
|
// Output control
|
|
this.controls.push(txt);
|
|
exp.append(lbl.element);
|
|
exp.append(txt.element);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Event Handlers //////////////////////////////
|
|
|
|
// Expand/collapse button key press
|
|
expKeyDown(e) {
|
|
if (
|
|
!(e.altKey || e.ctrlKey || e.shiftKey) &&
|
|
(e.key == " " || e.key == "Enter")
|
|
) this.expanded = !this.expanded;
|
|
}
|
|
|
|
// Expand/collapse button pointer down
|
|
expPointerDown(e) {
|
|
this.btnExpand.element.focus();
|
|
if (e.button != 0)
|
|
return;
|
|
Toolkit.handle(e);
|
|
this.expanded = !this.expanded;
|
|
}
|
|
|
|
// Program register format changed
|
|
onProgram(format) {
|
|
this.format = format;
|
|
this.txtValue.element.classList
|
|
[format == Register.HEX ? "add" : "remove"]("mono");
|
|
this.formatValue();
|
|
}
|
|
|
|
// Bit check box input
|
|
onBit(e) {
|
|
let oldValue = this.target[this.key];
|
|
let target = e.target.component;
|
|
let mask = 1 << target.bit;
|
|
|
|
// Cannot change the value
|
|
if (e.disabled)
|
|
return;
|
|
|
|
// Update the value
|
|
this.setValue(target.checked ? oldValue | mask : oldValue & ~mask);
|
|
}
|
|
|
|
// Text box commit
|
|
onText(e) {
|
|
let oldValue = this.target[this.key];
|
|
let target = e.target.component;
|
|
let newValue = parseInt(target.value, target.isHex ? 16 : 10);
|
|
|
|
// Cannot change the value
|
|
if (e.disabled)
|
|
return;
|
|
|
|
// The provided value is invalid
|
|
if (!Number.isInteger(newValue)) {
|
|
this.refresh();
|
|
return;
|
|
}
|
|
|
|
// Update the value
|
|
let mask = (1 << target.bits) - 1 << target.bit;
|
|
this.setValue(oldValue & ~mask | newValue << target.bit & mask);
|
|
}
|
|
|
|
// Value text box commit
|
|
valAction(e) {
|
|
let text = this.txtValue.value;
|
|
let value = null;
|
|
|
|
Toolkit.handle(e);
|
|
|
|
// Program register with non-default format
|
|
if (this.type == Register.PROGRAM && this.format != Register.HEX) {
|
|
switch (this.format) {
|
|
case Register.SIGNED:
|
|
value = parseInt(text);
|
|
if (
|
|
!Number.isInteger(value) ||
|
|
value < -2147483648 ||
|
|
value > +2147483647
|
|
) value = null;
|
|
break;
|
|
case Register.UNSIGNED:
|
|
value = parseInt(text);
|
|
if (
|
|
!Number.isInteger(value) ||
|
|
value < 0 ||
|
|
value > 4294967295
|
|
) value = null;
|
|
break;
|
|
case Register.FLOAT:
|
|
value = parseFloat(text);
|
|
value =
|
|
!Number.isFinite(value) ||
|
|
value < Debugger.ixf(0xFF7FFFFF) ||
|
|
value > Debugger.ixf(0x7F7FFFFF)
|
|
? null : Debugger.fxi(value) >>> 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Default hexadecimal format
|
|
else {
|
|
value = parseInt(text, 16);
|
|
if (
|
|
!Number.isInteger(value) ||
|
|
value < 0 ||
|
|
value > 4294967295
|
|
) value = null;
|
|
}
|
|
|
|
// Apply the new value
|
|
if (value === null)
|
|
this.formatValue();
|
|
else this.setValue(value);
|
|
}
|
|
|
|
// Value text box key press
|
|
valKeyDown(e) {
|
|
if (e.altKey || e.ctrlKey || e.shiftKey || e.key != "Escape")
|
|
return;
|
|
Toolkit.handle(e);
|
|
this.txtValue.value =
|
|
this.debug.dasm.hex(this.target[this.key], 8, false);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Public Methods //////////////////////////////
|
|
|
|
// The expansion area is visible
|
|
get expanded() {
|
|
return this.btnExpand.element.getAttribute("aria-expanded") == "true";
|
|
}
|
|
set expanded(expanded) {
|
|
expanded = !!expanded;
|
|
if (this.expansion == null || expanded == this.expanded)
|
|
return;
|
|
this.btnExpand.element.setAttribute("aria-expanded", expanded);
|
|
this.expansion.visible = expanded;
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Package Methods /////////////////////////////
|
|
|
|
// Update controls from simulation state
|
|
refresh() {
|
|
|
|
// Name label
|
|
this.label.setText(
|
|
this.key == "pc" ? "PC" :
|
|
this.type != Register.PROGRAM ? Disassembler.SYSNAMES[this.key] :
|
|
this.dasm.programRegister(this.key)
|
|
);
|
|
|
|
// Value text box
|
|
let value = this.target[this.key];
|
|
this.formatValue();
|
|
|
|
// Expansion controls
|
|
for (let ctrl of this.controls) {
|
|
|
|
// Bit check box
|
|
if (ctrl instanceof Toolkit.Checkbox)
|
|
ctrl.checked = !!(value >> ctrl.bit & 1);
|
|
|
|
// Decimal text box
|
|
else if (ctrl instanceof Toolkit.TextBox && !ctrl.isHex)
|
|
ctrl.value = value >> ctrl.bit & (1 << ctrl.bits) - 1;
|
|
|
|
// Hexadecimal text box
|
|
else if (ctrl instanceof Toolkit.TextBox && ctrl.isHex) {
|
|
ctrl.value = this.dasm.hex(
|
|
value >> ctrl.bit & (1 << ctrl.bits) - 1,
|
|
Math.ceil(ctrl.bits / 4),
|
|
false);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Private Methods /////////////////////////////
|
|
|
|
// Format the value as a string in the text box
|
|
formatValue() {
|
|
let text = "";
|
|
let value = this.target[this.key];
|
|
|
|
// Program register with non-default format
|
|
if (this.type == Register.PROGRAM && this.format != Register.HEX) {
|
|
switch (this.format) {
|
|
case Register.SIGNED:
|
|
text = (value >> 0).toString();
|
|
break;
|
|
case Register.UNSIGNED:
|
|
text = (value >>> 0).toString();
|
|
break;
|
|
case Register.FLOAT:
|
|
value = Debugger.ixf(value);
|
|
if (Number.isFinite(value)) {
|
|
text = value.toFixed(100);
|
|
if (/[^0-9\-\.]/.test(text))
|
|
text = value.toFixed(6);
|
|
if (text.indexOf(".") != -1) {
|
|
text = text.replace(/0+$/, "")
|
|
.replace(/\.$/, ".0");
|
|
} else text += ".0";
|
|
} else if (!Number.isNaN(value)) {
|
|
text =
|
|
(value == Number.NEGATIVE_INFINITY ? "-" : "") +
|
|
this.debug.app.localize("{debug.cpu.infinity}")
|
|
;
|
|
} else text = "NaN";
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Default hexadecimal format
|
|
else text = this.dasm.hex(value >>> 0, 8, false);
|
|
|
|
// Update the text
|
|
this.txtValue.value = text;
|
|
}
|
|
|
|
// Specify a new register value
|
|
async setValue(value) {
|
|
|
|
// Update the value in the simulation state
|
|
let sim = this.debug.sim;
|
|
let result = await (
|
|
this.key == "pc" ? this.debug.core
|
|
.setProgramCounter(sim, value, {
|
|
refresh: [ this.registers.cpu.disassembler.subscription ]
|
|
}) :
|
|
this.type != Register.PROGRAM ? this.debug.core
|
|
.setSystemRegister(sim, this.key, value) :
|
|
this.debug.core
|
|
.setProgramRegister(sim, this.key, value)
|
|
);
|
|
|
|
// Update the value in the debugger window
|
|
this.target[this.key] = result.value;
|
|
|
|
// Update the register controls
|
|
this.refresh();
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
// Register list manager
|
|
class RegisterPane extends Toolkit.SplitPane {
|
|
|
|
//////////////////////////////// Constants ////////////////////////////////
|
|
|
|
// System register templates
|
|
static SYSTEMS = [
|
|
[ [ "pc" ], Register.PLAIN ],
|
|
[ [ "system", 5 ], Register.PSW ],
|
|
[ [ "system", 25 ], Register.PLAIN ],
|
|
[ [ "system", 24 ], Register.CHCW ],
|
|
[ [ "system", 4 ], Register.ECR ],
|
|
[ [ "system", 0 ], Register.PLAIN ],
|
|
[ [ "system", 1 ], Register.PSW ],
|
|
[ [ "system", 2 ], Register.PLAIN ],
|
|
[ [ "system", 3 ], Register.PSW ],
|
|
[ [ "system", 6 ], Register.PIR ],
|
|
[ [ "system", 7 ], Register.TKCW ],
|
|
[ [ "system", 29 ], Register.PLAIN ],
|
|
[ [ "system", 30 ], Register.PLAIN ],
|
|
[ [ "system", 31 ], Register.PLAIN ]
|
|
];
|
|
|
|
|
|
|
|
///////////////////////// Initialization Methods //////////////////////////
|
|
|
|
constructor(cpu) {
|
|
super(cpu.debug.app, {
|
|
orientation: "top",
|
|
style: {
|
|
overflow: "visible"
|
|
}
|
|
});
|
|
|
|
// Configure instance fields
|
|
this.cpu = cpu;
|
|
this.list = [];
|
|
this.pc = 0xFFFFFFF0;
|
|
this.program = new Array(32);
|
|
this.pending = false;
|
|
this.subscription = [ 0, cpu.index, "cpu", "registers", "refresh" ],
|
|
this.system = new Array(32);
|
|
|
|
// Initialize regsiters
|
|
for (let x = 0; x < 32; x++)
|
|
this.program[x] = this.system[x] = 0;
|
|
|
|
// System registers list
|
|
this.lstSystem = new Toolkit.Component(cpu.debug.app, {
|
|
class: "tk registers",
|
|
style: {
|
|
minHeight: "100%",
|
|
minWidth : "100%",
|
|
width : "max-content"
|
|
}
|
|
});
|
|
|
|
// System registers scroll pane
|
|
this.scrSystem = new Toolkit.ScrollPane(cpu.debug.app, {
|
|
class : "tk scroll-pane scr-system",
|
|
overflowX: "auto",
|
|
overflowY: "scroll",
|
|
view : this.lstSystem,
|
|
style : {
|
|
position: "relative"
|
|
}
|
|
});
|
|
this.primary = this.scrSystem;
|
|
|
|
// Program registers list
|
|
this.lstProgram = new Toolkit.Component(cpu.debug.app, {
|
|
class: "tk registers",
|
|
style: {
|
|
minHeight: "100%",
|
|
minWidth : "100%",
|
|
width : "max-content"
|
|
}
|
|
});
|
|
|
|
// Program registers scroll pane
|
|
this.scrProgram = new Toolkit.ScrollPane(cpu.debug.app, {
|
|
class : "tk scroll-pane scr-program",
|
|
overflowX: "auto",
|
|
overflowY: "scroll",
|
|
view : this.lstProgram
|
|
});
|
|
this.secondary = this.scrProgram;
|
|
|
|
// Configure register lists
|
|
for (let sys of RegisterPane.SYSTEMS)
|
|
this.addRegister(new Register(this, sys[0], sys[1]));
|
|
for (let x = 0; x < 32; x++) {
|
|
this.addRegister(new Register(this,
|
|
[ "program", x ], Register.PROGRAM));
|
|
}
|
|
|
|
// Value text box measurer
|
|
let text = [];
|
|
for (let c of "0123456789abcdefABCDEF")
|
|
text.push(c.repeat(8));
|
|
this.sizer = new Toolkit.Label(cpu.app, {
|
|
class: "tk text-box mono",
|
|
style: {
|
|
position : "absolute",
|
|
visibility: "hidden"
|
|
}
|
|
});
|
|
this.sizer.setText(text.join("\n"), false);
|
|
this.list[0].main.element.after(this.sizer.element);
|
|
|
|
// Monitor the bounds of the register names column
|
|
let resizer = new ResizeObserver(()=>this.regResize());
|
|
for (let reg of this.list)
|
|
resizer.observe(reg.label.element);
|
|
|
|
// Monitor the bounds of the value text boxes
|
|
this.list[0].txtValue.addEventListener("resize", e=>this.valResize(e));
|
|
this.sizer .addEventListener("resize", e=>this.sizResize(e));
|
|
}
|
|
|
|
// Add a Register object to a list
|
|
addRegister(reg) {
|
|
this.list.push(reg);
|
|
|
|
// Add the main element to the appropriate list container
|
|
let list = this[reg.type == Register.PROGRAM ?
|
|
"lstProgram" : "lstSystem"];
|
|
list.add(reg.main);
|
|
|
|
// Add the expansion element
|
|
if (reg.expansion != null)
|
|
list.add(reg.expansion);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Event Handlers //////////////////////////////
|
|
|
|
// Register label resized
|
|
regResize() {
|
|
let max = 0;
|
|
let widths = new Array(this.list.length);
|
|
|
|
// Measure the widths of all labels
|
|
for (let x = 0; x < this.list.length; x++) {
|
|
widths[x] = Math.ceil(this.list[x].label.element
|
|
.getBoundingClientRect().width);
|
|
max = Math.max(max, widths[x]);
|
|
}
|
|
|
|
// Ensure all labels share the same maximum width
|
|
for (let x = 0; x < this.list.length; x++) {
|
|
if (widths[x] < max)
|
|
this.list[x].label.element.style.minWidth = max + "px";
|
|
}
|
|
|
|
}
|
|
|
|
// Sizer resized
|
|
sizResize(e) {
|
|
let width = Math.ceil(e.target.getBoundingClientRect().width) + "px";
|
|
for (let reg of this.list)
|
|
reg.txtValue.style.minWidth = width;
|
|
}
|
|
|
|
// Value text box resized
|
|
valResize(e) {
|
|
let height = Math.ceil(e.target.getBoundingClientRect().height);
|
|
this.scrSystem .vscroll.unitIncrement = height;
|
|
this.scrProgram.vscroll.unitIncrement = height;
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Package Methods /////////////////////////////
|
|
|
|
// Retrieve registers from the simulation state
|
|
async fetch() {
|
|
|
|
// Select the parameters for the simulation fetch
|
|
let params = {};
|
|
|
|
// A communication with the core thread is already underway
|
|
if (this.pending) {
|
|
this.pending = params;
|
|
this.refresh();
|
|
return;
|
|
}
|
|
|
|
// Retrieve data from the simulation state
|
|
this.pending = params;
|
|
for (let data = null, promise = null; this.pending instanceof Object;){
|
|
|
|
// Wait for a transaction to complete
|
|
if (promise != null) {
|
|
this.pending= true;
|
|
data = await promise;
|
|
promise = null;
|
|
}
|
|
|
|
// Initiate a new transaction
|
|
if (this.pending instanceof Object) {
|
|
params = this.pending;
|
|
let options = {};
|
|
if (this.isVisible())
|
|
options.subscription = this.subscription;
|
|
promise = this.cpu.debug.core.getAllRegisters(
|
|
this.cpu.debug.sim, options);
|
|
}
|
|
|
|
// Process the result of a transaction
|
|
if (data != null) {
|
|
this.refresh(data);
|
|
data = null;
|
|
}
|
|
|
|
};
|
|
this.pending = false;
|
|
}
|
|
|
|
// Component is being displayed for the first time
|
|
firstShow() {
|
|
|
|
// Retrieve the desired dimensions of the system registers list
|
|
let bounds = this.scrSystem.element.getBoundingClientRect();
|
|
for (let reg of this.list) {
|
|
let style = reg.main.style;
|
|
if (style.visibility) {
|
|
style.removeProperty("position");
|
|
style.removeProperty("visibility");
|
|
}
|
|
}
|
|
|
|
// Prepare the initial dimensions of the register lists
|
|
this .element.style.width = Math.ceil(bounds.width ) + "px";
|
|
this.scrSystem.element.style.height = Math.ceil(bounds.height) + "px";
|
|
}
|
|
|
|
// Update register lists
|
|
refresh(msg = null) {
|
|
|
|
// Receiving data from the simulation state
|
|
if (msg != null) {
|
|
this.pc = msg.pc;
|
|
this.program.splice(0, this.program.length, ... msg.program);
|
|
this.system .splice(0, this.system .length, ... msg.system );
|
|
}
|
|
|
|
// Update register controls
|
|
for (let reg of this.list)
|
|
reg.refresh();
|
|
}
|
|
|
|
// Stop receiving updates from the simulation
|
|
unsubscribe() {
|
|
this.cpu.debug.core.unsubscribe(this.subscription, false);
|
|
}
|
|
|
|
}
|
|
|
|
Debugger.CPU = CPU;
|
|
}
|
|
|
|
export { register };
|