diff --git a/css/ui.css b/css/ui.css index 5edbf832..8cae3bd0 100644 --- a/css/ui.css +++ b/css/ui.css @@ -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%; } - \ No newline at end of file +.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;} diff --git a/presets/zmachine/skeleton.inform6 b/presets/zmachine/skeleton.inform6 new file mode 100644 index 00000000..ad7b6638 --- /dev/null +++ b/presets/zmachine/skeleton.inform6 @@ -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"; + diff --git a/src/common/baseplatform.ts b/src/common/baseplatform.ts index b124e3e0..2c135f42 100644 --- a/src/common/baseplatform.ts +++ b/src/common/baseplatform.ts @@ -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 { diff --git a/src/common/workertypes.ts b/src/common/workertypes.ts index 628ff3b0..5848e0c4 100644 --- a/src/common/workertypes.ts +++ b/src/common/workertypes.ts @@ -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 } diff --git a/src/ide/ui.ts b/src/ide/ui.ts index 0a27650a..44ef7aa9 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -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; diff --git a/src/ide/views.ts b/src/ide/views.ts index 226d41c8..019aeb67 100644 --- a/src/ide/views.ts +++ b/src/ide/views.ts @@ -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; + 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 { diff --git a/src/platform/verilog.ts b/src/platform/verilog.ts index b1b451b3..1b37b017 100644 --- a/src/platform/verilog.ts +++ b/src/platform/verilog.ts @@ -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 = { diff --git a/src/platform/zmachine.ts b/src/platform/zmachine.ts index 711df06b..45f28be4 100644 --- a/src/platform/zmachine.ts +++ b/src/platform/zmachine.ts @@ -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; + } } // diff --git a/src/worker/workermain.ts b/src/worker/workermain.ts index f1feb0a3..1dd737e0 100644 --- a/src/worker/workermain.ts +++ b/src/worker/workermain.ts @@ -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 = {