working on tree view for state, extra debuginfo

This commit is contained in:
Steven Hugg 2020-07-07 20:56:44 -05:00
parent 741df9f5b8
commit 71fa79cec5
9 changed files with 416 additions and 112 deletions

View File

@ -594,8 +594,8 @@ div.asset_toolbar {
font-weight: bold;
}
.transcript-style-8 { /* input */
font-weight: bold;
font-variant: small-caps;
font-weight: 600;
text-transform: uppercase;
color: #6666ff;
background-color: #eeeeff;
padding: 0.25em;
@ -607,10 +607,54 @@ div.asset_toolbar {
color: #ddd;
}
.transcript-input {
margin:1%;
font-weight: bold;
font-variant: small-caps;
font-weight: 600;
text-transform: uppercase;
color: #6666ff;
background-color: #eeeeff;
margin:1%;
}
.tree-header {
border: 2px solid #555;
border-radius:8px;
color: #fff;
background-color:#666;
padding-left:1em;
font-family: "Andale Mono", "Menlo", "Lucida Console", monospace;
}
.tree-content {
padding-left:0.75em;
padding-right:0.75em;
font-size: small;
}
.tree-value {
float:right;
font-weight:normal;
padding-right:1em;
}
.tree-expanded::after {
float: right;
margin-right: 1em;
content: '\25b2';
}
.tree-collapsed::after {
float: right;
margin-right: 1em;
content: '\25bc';
}
.tree-collapsed:hover, .tree-expanded:hover {
border-color: rgba(255,255,255,0.7);
}
.tree-header:hover {
background-color: rgba(255,255,255,0.3);
}
.tree-level-0 { display:none;}
.tree-level-1 { background-color: #638283;}
.tree-level-2 { background-color: #636e83;}
.tree-level-3 { background-color: #636483;}
.tree-level-4 { background-color: #756383;}
.tree-level-5 { background-color: #83637e;}
.tree-level-6 { background-color: #83636e;}
.tree-level-7 { background-color: #836363;}
.tree-level-8 { background-color: #837163;}
.tree-level-9 { background-color: #7b8363;}
.tree-level-10 { background-color: #738363;}

View File

@ -0,0 +1,39 @@
Constant Story "My Story Name";
Constant Headline
"^This is My Story^
By New Writer (2020)^";
Constant MAX_SCORE 100;
Release 1;
Include "Parser";
Include "VerbLib";
!-------------------------------------------------------------------------------
! Initialise
!-------------------------------------------------------------------------------
[ Initialise;
location = Main_Lobby;
];
! ----------------------------------------------------------------------------
! Locations
! ----------------------------------------------------------------------------
Object Main_Lobby "Main Lobby"
with description
"You are in the main lobby.",
has light;
! ----------------------------------------------------------------------------
! Grammar
! ----------------------------------------------------------------------------
Include "Grammar";

View File

@ -45,9 +45,11 @@ export type AddrSymbolMap = {[address:number]:string};
export class DebugSymbols {
symbolmap : SymbolMap; // symbol -> address
addr2symbol : AddrSymbolMap; // address -> symbol
debuginfo : {}; // extra platform-specific debug info
constructor(symbolmap : SymbolMap) {
constructor(symbolmap : SymbolMap, debuginfo : {}) {
this.symbolmap = symbolmap;
this.debuginfo = debuginfo;
this.addr2symbol = invertMap(symbolmap);
//// TODO: shouldn't be necc.
if (!this.addr2symbol[0x0]) this.addr2symbol[0x0] = '$00'; // needed for ...
@ -123,6 +125,7 @@ export interface Platform {
getCPUState?() : CpuState;
debugSymbols? : DebugSymbols;
getDebugTree?() : {};
startProbing?() : ProbeRecorder;
stopProbing?() : void;
@ -194,6 +197,9 @@ export abstract class BasePlatform {
inspect(sym: string) : string {
return inspectSymbol((this as any) as Platform, sym);
}
getDebugTree() {
return this.saveState();
}
}
export abstract class BaseDebugPlatform extends BasePlatform {

View File

@ -96,12 +96,13 @@ export type WorkerOutput = Uint8Array | VerilogOutput;
export type Segment = {name:string, start:number, size:number, last?:number, type?:string};
export interface WorkerResult {
output:WorkerOutput,
errors:WorkerError[],
listings:CodeListingMap,
symbolmap:{[sym:string]:number},
params:{},
segments?:Segment[],
unchanged?:boolean,
output:WorkerOutput
errors:WorkerError[]
listings:CodeListingMap
symbolmap:{[sym:string]:number}
params:{}
segments?:Segment[]
unchanged?:boolean
debuginfo?:{} // optional info
}

View File

@ -278,6 +278,11 @@ function refreshWindowList() {
return new Views.VRAMMemoryView();
});
}
if (platform.getDebugTree) {
addWindowItem("#debugview", "Debug Browser", () => {
return new Views.DebugBrowserView();
});
}
if (platform.startProbing) {
addWindowItem("#memheatmap", "Memory Probe", () => {
return new Views.AddressHeatMapView();
@ -1031,7 +1036,7 @@ function setCompileOutput(data: WorkerResult) {
showErrorAlert(data.errors);
} else {
// process symbol map
platform.debugSymbols = new DebugSymbols(data.symbolmap);
platform.debugSymbols = new DebugSymbols(data.symbolmap, data.debuginfo);
compparams = data.params;
// load ROM
var rom = data.output;

View File

@ -6,7 +6,7 @@ import { hex, lpad, rpad, safeident, rgb2bgr } from "../common/util";
import { CodeAnalyzer } from "../common/analysis";
import { platform, platform_id, compparams, current_project, lastDebugState, projectWindows, runToPC } from "./ui";
import { ProbeRecorder, ProbeFlags } from "../common/recorder";
import { getMousePos } from "../common/emu";
import { getMousePos, dumpRAM } from "../common/emu";
import * as pixed from "./pixeleditor";
declare var Mousetrap;
@ -809,7 +809,7 @@ export class VRAMMemoryView extends MemoryView {
///
export class BinaryFileView implements ProjectView {
memorylist;
vlist : VirtualTextScroller;
maindiv : HTMLElement;
path:string;
data:Uint8Array;
@ -821,30 +821,12 @@ export class BinaryFileView implements ProjectView {
}
createDiv(parent : HTMLElement) {
var div = document.createElement('div');
div.setAttribute("class", "memdump");
parent.appendChild(div);
this.showMemoryWindow(parent, div);
return this.maindiv = div;
this.vlist = new VirtualTextScroller(parent);
this.vlist.create(parent, ((this.data.length+15) >> 4), this.getMemoryLineAt.bind(this));
return this.vlist.maindiv;
}
showMemoryWindow(workspace:HTMLElement, parent:HTMLElement) {
this.memorylist = new VirtualList({
w: $(workspace).width(),
h: $(workspace).height(),
itemHeight: getVisibleEditorLineHeight(),
totalRows: ((this.data.length+15) >> 4),
generatorFn: (row : number) => {
var s = this.getMemoryLineAt(row);
var linediv = document.createElement("div");
linediv.appendChild(document.createTextNode(s));
return linediv;
}
});
$(parent).append(this.memorylist.container);
}
getMemoryLineAt(row : number) : string {
getMemoryLineAt(row : number) : VirtualTextLine {
var offset = row * 16;
var n1 = 0;
var n2 = 16;
@ -856,12 +838,13 @@ export class BinaryFileView implements ProjectView {
if (i==8) s += ' ';
s += ' ' + (read>=0?hex(read,2):' ');
}
return s;
return {text:s};
}
refresh() {
this.vlist.refresh();
}
getPath() { return this.path; }
}
@ -1216,41 +1199,26 @@ export class RasterStackMapView extends ProbeBitmapViewBase implements ProjectVi
}
export class ProbeLogView extends ProbeViewBaseBase {
memorylist;
vlist : VirtualTextScroller;
maindiv : HTMLElement;
recreateOnResize = true;
dumplines;
createDiv(parent : HTMLElement) {
var div = document.createElement('div');
div.setAttribute("class", "memdump");
parent.appendChild(div);
this.showMemoryWindow(parent, div);
return this.maindiv = div;
this.vlist = new VirtualTextScroller(parent);
this.vlist.create(parent, 160*262, this.getMemoryLineAt.bind(this));
return this.vlist.maindiv;
}
showMemoryWindow(workspace:HTMLElement, parent:HTMLElement) {
this.memorylist = new VirtualList({
w: $(workspace).width(),
h: $(workspace).height(),
itemHeight: getVisibleEditorLineHeight(),
totalRows: 160*262, // TODO?
generatorFn: (row : number) => {
var s = this.getMemoryLineAt(row);
var linediv = document.createElement("div");
linediv.appendChild(document.createTextNode(s));
return linediv;
}
});
$(parent).append(this.memorylist.container);
}
getMemoryLineAt(row : number) : string {
getMemoryLineAt(row : number) : VirtualTextLine {
var s : string = "";
var c : string = "seg_data";
var line = this.dumplines && this.dumplines[row];
if (line != null) {
var xtra = line.info.join(", ");
return "(" + lpad(line.row,3) + ", " + lpad(line.col,3) + ") " + rpad(line.asm||"",20) + xtra;
} else return "";
var xtra : string = line.info.join(", ");
s = "(" + lpad(line.row,3) + ", " + lpad(line.col,3) + ") " + rpad(line.asm||"",20) + xtra;
if (xtra.indexOf("Write ") >= 0) c = "seg_io";
}
return {text:s, clas:c};
}
refresh() {
this.tick();
@ -1279,17 +1247,7 @@ export class ProbeLogView extends ProbeViewBaseBase {
break;
}
});
// TODO: refactor with elsewhere
if (this.memorylist) {
$(this.maindiv).find('[data-index]').each( (i,e) => {
var div = $(e);
var row = parseInt(div.attr('data-index'));
var oldtext = div.text();
var newtext = this.getMemoryLineAt(row);
if (oldtext != newtext)
div.text(newtext);
});
}
this.vlist.refresh();
}
}
@ -1362,6 +1320,166 @@ export class ProbeSymbolView extends ProbeViewBaseBase {
}
}
///
const MAX_CHILDREN = 200;
const MAX_STRING_LEN = 100;
const MAX_DUMP_BYTES = 256;
class TreeNode {
parent : TreeNode;
name : string;
_div : HTMLElement;
_header : HTMLElement;
_inline : HTMLElement;
_content : HTMLElement;
children : Map<string,TreeNode>;
expanded = false;
level : number;
view : TreeViewBase;
constructor(parent : TreeNode, name : string) {
this.parent = parent;
this.name = name;
this.children = new Map();
this.level = parent ? (parent.level+1) : -1;
this.view = parent ? parent.view : null;
}
getDiv() {
if (this._div == null) {
this._div = document.createElement("div");
this._div.classList.add("vertical-scroll");
this._div.classList.add("tree-content");
this._header = document.createElement("div");
this._header.classList.add("tree-header");
this._header.classList.add("tree-level-" + this.level);
this._header.append(this.name);
this._inline = document.createElement("span");
this._inline.classList.add("tree-value");
this._header.append(this._inline);
this._div.append(this._header);
this.parent._content.append(this._div);
this._header.onclick = (e) => {
this.toggleExpanded();
};
}
if (this.expanded && this._content == null) {
this._content = document.createElement("div");
this._div.append(this._content);
}
else if (!this.expanded && this._content != null) {
this._content.remove();
this._content = null;
this.children.clear();
}
return this._div;
}
toggleExpanded() {
this.expanded = !this.expanded;
this.view.tick();
}
remove() {
this._div.remove();
this._div = null;
}
update(obj : any) {
this.getDiv();
var text = "";
// is it a function? call it first, if we are expanded
if (typeof obj == 'function' && this._content != null) {
obj = obj();
}
// check null first
if (obj == null) {
text = obj+"";
// primitive types
} else if (typeof obj == 'number') {
text = obj.toString();
} else if (typeof obj == 'boolean') {
text = obj.toString();
} else if (typeof obj == 'string') {
if (obj.length < MAX_STRING_LEN)
text = obj;
else
text = obj.substring(0, MAX_STRING_LEN) + "...";
// byte array (TODO: other kinds)
} else if (obj instanceof Uint8Array && obj.length <= MAX_DUMP_BYTES) {
text = dumpRAM(obj, 0, obj.length);
// recurse into object? (or function)
} else if (typeof obj == 'object' || typeof obj == 'function') {
if (this._content != null) {
let names = Object.getOwnPropertyNames(obj);
if (names.length < MAX_CHILDREN) { // max # of child objects
let orphans = new Set(this.children.keys());
// visit all children
names.forEach((name) => {
let childnode = this.children.get(name);
if (childnode == null) {
childnode = new TreeNode(this, name);
this.children.set(name, childnode);
}
childnode.update(obj[name]);
orphans.delete(name);
});
// remove orphans
orphans.forEach((delname) => {
let childnode = this.children.get(delname);
childnode.remove();
this.children.delete(delname);
});
this._header.classList.add("tree-expanded");
this._header.classList.remove("tree-collapsed");
} else {
text = names.length + " items"; // too many children
}
} else {
this._header.classList.add("tree-collapsed");
this._header.classList.remove("tree-expanded");
}
} else {
text = typeof obj; // fallthrough
}
// change DOM object if needed
if (this._inline.innerText != text) {
this._inline.innerText = text;
}
}
}
export abstract class TreeViewBase implements ProjectView {
root : TreeNode;
createDiv(parent : HTMLElement) : HTMLElement {
var mainnode = new TreeNode(null, null);
mainnode.view = this;
mainnode._content = parent;
this.root = new TreeNode(mainnode, "/");
this.root.expanded = true;
this.root.getDiv(); // create it
this.root._div.style.padding = '0px';
return this.root.getDiv(); // should be cached
}
refresh() {
this.tick();
}
tick() {
this.root.update(this.getRootObject());
}
abstract getRootObject() : Object;
}
export class StateBrowserView extends TreeViewBase implements ProjectView {
getRootObject() { return platform.saveState(); }
}
export class DebugBrowserView extends TreeViewBase implements ProjectView {
getRootObject() { return platform.getDebugTree(); }
}
///
export class AssetEditorView implements ProjectView, pixed.EditorContext {

View File

@ -785,6 +785,10 @@ var VerilogPlatform = function(mainElement, options) {
// DEBUGGING
getDebugTree() {
return this.saveState().o;
}
// TODO: bind() a function to avoid depot?
saveState() {
var state = {

View File

@ -55,10 +55,9 @@ class GlkImpl {
curline: HTMLElement;
curstyle: number;
reverse: boolean;
windows: GlkWindow[];
wnd: GlkWindow;
waitingfor: "line" | "char" | null;
focused = false;
exited = false;
constructor(page: HTMLElement, input: HTMLInputElement) {
this.page = page;
@ -66,8 +65,7 @@ class GlkImpl {
this.reset();
}
reset() {
this.windows = [];
this.wnd = null;
this.exited = false;
this.clear();
}
clear() {
@ -89,6 +87,7 @@ class GlkImpl {
// TODO
}
glk_exit() {
this.exited = true;
this.flushline();
this.addtext("** Game exited **", 1);
}
@ -681,10 +680,11 @@ class ZmachinePlatform implements Platform {
*/
isRunning(): boolean {
return this.zvm != null;
return this.zvm != null && !this.glk.exited;
}
advance(novideo?: boolean): number {
// TODO?
// TODO? we should advance 1 step, whatever that is in ZVM
return 0;
}
@ -696,17 +696,106 @@ class ZmachinePlatform implements Platform {
}
showHelp(tool:string, ident?:string) {
switch (tool) {
case 'inform6': window.open("https://www.inform-fiction.org/manual/html/"); break;
case 'inform6': window.open("https://www.inform-fiction.org/manual/html/contents.html"); break;
}
}
getPresets(): Preset[] {
return ZMACHINE_PRESETS;
}
// TODO: Z machine is big endian!!
inspect(ident:string) {
return inspectSymbol(this, ident);
}
getDebugTree() {
var root = {};
//root['debuginfo'] = sym.debuginfo;
if (this.zvm != null) {
root['Objects'] = () => this.getRootObjects();
root['Globals'] = () => this.getGlobalVariables();
}
return root;
}
getObjectName(node) {
var objlookup = this.getDebugLookup('object');
var name = objlookup[node] || "";
name += " (#" + node + ")";
return name;
}
addObjectToTree(tree, child) {
let name = this.getObjectName(child);
tree[name] = this.getObjectTree(child);
}
getRootObjects() {
var tree = {};
// TODO: better way?
try {
for (let child=0; child<65536; child++) {
if (this.zvm.get_parent(child) == 0) {
this.addObjectToTree(tree, child);
}
}
} catch (e) {
if (!(e instanceof RangeError)) throw e;
}
return tree;
}
getObjectTree(parentobj: number) {
var child = this.zvm.get_child(parentobj);
var tree = {};
while (child) {
this.addObjectToTree(tree, child);
child = this.zvm.get_sibling(child);
}
// add attributes
var flags = this.getFlagList(parentobj);
if (flags.length) {
tree["[attributes]"] = flags.join(' ');
}
/*
var props = this.getPropList(parentobj);
if (props.length) {
tree["[properties]"] = props.join(' ');
}
*/
return tree;
}
getFlagList(obj:number) {
var attrlookup = this.getDebugLookup('attribute');
var set_attrs = [];
for (var i=0; i<32; i++) {
if (this.zvm.test_attr(obj, i)) {
set_attrs.push(attrlookup[i] || "#"+i);
}
}
return set_attrs;
}
getPropList(obj:number) {
var proplookup = this.getDebugLookup('property');
var set_props = [];
var addr = 0;
for (var i=0; i<50; i++) {
addr = this.zvm.find_prop(obj, 0, addr);
if (addr == 0) break;
set_props.push(proplookup[addr] || "%"+addr);
}
return set_props;
}
getDebugLookup(key : 'object'|'property'|'attribute'|'constant'|'global-variable') : {} {
var debugsym = (this as Platform).debugSymbols;
return (debugsym && debugsym.debuginfo && debugsym.debuginfo[key]) || {};
}
getGlobalVariables() {
var globals = this.getDebugLookup('global-variable');
var result = {};
Object.entries(globals).forEach((entry) => {
var addr = parseInt(entry[0]);
var name = entry[1] as string;
result[name] = this.zvm.m.getUint16(addr);
})
return result;
}
}
//

View File

@ -1,6 +1,6 @@
"use strict";
import { WorkerResult, WorkerFileUpdate, WorkerBuildStep, WorkerMessage, WorkerError, Dependency, SourceLine, CodeListing, CodeListingMap, Segment } from "../common/workertypes";
import { WorkerResult, WorkerFileUpdate, WorkerBuildStep, WorkerMessage, WorkerError, Dependency, SourceLine, CodeListing, CodeListingMap, Segment, WorkerOutput } from "../common/workertypes";
declare var WebAssembly;
declare function importScripts(path:string);
@ -2397,9 +2397,9 @@ interface XMLNode {
function parseXMLPoorly(s: string) : XMLNode {
var re = /[<]([/]?)([?a-z_-]+)([^>]*)[>]+|(\s*[^<]+)/gi;
var m;
var i=0;
var stack = [];
var m : RegExpMatchArray;
//var i=0;
var stack : XMLNode[] = [];
while (m = re.exec(s)) {
var [_m0,close,ident,attrs,content] = m;
//if (i++<100) console.log(close,ident,attrs,content);
@ -2432,7 +2432,7 @@ function compileInform6(step:BuildStep) {
lstout += "\n";
}
}
var args = [ '-afnops', '-v5', '-Cu', '-E1', '-k', '+/share/lib', step.path ];
var args = [ '-afjnops', '-v5', '-Cu', '-E1', '-k', '+/share/lib', step.path ];
var inform = emglobal.inform({
instantiateWasm: moduleInstFn('inform'),
noInitialRun:true,
@ -2454,10 +2454,15 @@ function compileInform6(step:BuildStep) {
// parse debug XML
var symbolmap = {};
var entitymap = {'object':{}, 'property':{}, 'constant':{}};
var segments : Segment[] = [];
var entitymap = {
// number -> string
'object':{}, 'property':{}, 'attribute':{}, 'constant':{}, 'global-variable':{}, 'routine':{},
};
var dbgout = FS.readFile("gameinfo.dbg", {encoding:'utf8'});
var xmlroot = parseXMLPoorly(dbgout);
//console.log(xmlroot);
var segtype = "ram";
xmlroot.children.forEach((node) => {
switch (node.type) {
case 'global-variable':
@ -2465,34 +2470,26 @@ function compileInform6(step:BuildStep) {
var ident = node.children.find((c,v) => c.type=='identifier').text;
var address = parseInt(node.children.find((c,v) => c.type=='address').text);
symbolmap[ident] = address;
entitymap[node.type][address] = ident;
break;
case 'object':
case 'property':
case 'attribute':
var ident = node.children.find((c,v) => c.type=='identifier').text;
var value = parseInt(node.children.find((c,v) => c.type=='value').text);
entitymap[node.type][ident] = value;
//entitymap[node.type][ident] = value;
entitymap[node.type][value] = ident;
//symbolmap[ident] = address | 0x1000000;
break;
case 'story-file-section':
var name = node.children.find((c,v) => c.type=='type').text;
var address = parseInt(node.children.find((c,v) => c.type=='address').text);
var endAddress = parseInt(node.children.find((c,v) => c.type=='end-address').text);
if (name == "grammar table") segtype = "rom";
segments.push({name:name, start:address, size:endAddress-address, type:segtype});
}
});
// parse segments
var segments : Segment[] = [];
var seglst = lstout.split("Offsets in story file:")[1];
if (seglst) {
let curseg : Segment = {name:'Header',start:0x0,size:0x42,type:'rom'};
segments.push(curseg);
let curtype = 'ram';
let re_seg = /([0-9a-f]{5}) (\w+)/g;
let m;
while (m = re_seg.exec(seglst)) {
var start = parseInt(m[1], 16);
var name = m[2];
if (name == 'Parse') curtype = 'rom';
curseg.size = start - curseg.start;
curseg = {name:name, start:start, size:0, type:curtype};
segments.push(curseg);
}
}
// parse listing
var listings : CodeListingMap = {};
// 35 +00015 <*> call_vs long_19 location long_424 -> sp
var lines = parseListing(lstout, /\s*(\d+)\s+[+]([0-9a-f]+)\s+([<*>]*)\s*(\w+)\s+(.+)/i, -1, 2, 4);
@ -2504,10 +2501,11 @@ function compileInform6(step:BuildStep) {
errors:errors,
symbolmap:symbolmap,
segments:segments,
//debuginfo:entitymap,
debuginfo:entitymap,
};
}
}
////////////////////////////
var TOOLS = {