pvbemu/web/debugger/CPU.js

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 };