diff --git a/Gruntfile.js b/Gruntfile.js index 744effce6..b9619dfc0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -114,6 +114,7 @@ module.exports = function(grunt) { * @property {Array.} c1pJSFiles * @property {Array.} pcX86Files * @property {Array.} pc8080Files + * @property {Array.} pdp10Files * @property {Array.} pdp11Files * @property {Array.} closureCompilerExterns */ @@ -122,6 +123,7 @@ module.exports = function(grunt) { var tmpC1Pjs = "./tmp/c1pjs/" + pkg.version + "/c1p.js"; var tmpPCx86 = "./tmp/pcx86/" + pkg.version + "/pcx86.js"; var tmpPC8080 = "./tmp/pc8080/" + pkg.version + "/pc8080.js"; + var tmpPDP10 = "./tmp/pdpjs/" + pkg.version + "/pdp10.js"; var tmpPDP11 = "./tmp/pdpjs/" + pkg.version + "/pdp11.js"; grunt.initConfig({ @@ -176,6 +178,19 @@ module.exports = function(grunt) { src: pkg.pc8080Files, dest: "./versions/pc8080/" + pkg.version + "/pc8080-dbg.js" }, + "pdp10.js": { + src: pkg.pdp10Files, + dest: "./versions/pdpjs/" + pkg.version + "/pdp10.js", + options: { + process: function(src, filepath) { + return (path.basename(filepath) == "debugger.js"? "" : src); + } + } + }, + "pdp10-dbg.js": { + src: pkg.pdp10Files, + dest: "./versions/pdpjs/" + pkg.version + "/pdp10-dbg.js" + }, "pdp11.js": { src: pkg.pdp11Files, dest: "./versions/pdpjs/" + pkg.version + "/pdp11.js", @@ -243,6 +258,24 @@ module.exports = function(grunt) { } } }, + "tmp-pdp10": { + src: pkg.pdp10Files, + dest: tmpPDP10, + options: { + banner: '"use strict";\n\n', + process: function(src, filepath) { + return "/**\n * @copyright " + filepath.replace(/^\./, "http://pcjs.org") + " (C) Jeff Parsons 2012-2017\n */\n\n" + + src.replace(/(^|\n)[ \t]*(['"])use strict\2;?/g, '') + .replace(/^(import|export)[ \t]+[^\n]*\n/gm, '') + .replace(/^[ \t]*var\s+\S+\s*=\s*require\((['"]).*?\1\);/gm, '') + .replace(/^[ \t]*(if\s+\(NODE\)\s*|)module\.exports\s*=\s*\S+;/gm, '') + .replace(/\/\*\*\s*\*\s*@fileoverview[\s\S]*?\*\/\s*/g, '') + .replace(/[ \t]*if\s*\(NODE\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '') + .replace(/[ \t]*if\s*\(typeof\s+module\s*!==\s*(['"])undefined\1\)\s*(\{[^}]*}|[^\n]*)(\n|$)/gm, '') + .replace(/[ \t]*[A-Za-z_][A-Za-z0-9_.]*\.assert\([^\n]*\);[^\n]*/g, ''); + } + } + }, "tmp-pdp11": { src: pkg.pdp11Files, dest: tmpPDP11, @@ -281,11 +314,14 @@ module.exports = function(grunt) { src: pkg.pc8080Files, dest: tmpPC8080 }, + "pdp10.js": { + src: pkg.pdp10Files, + dest: tmpPDP10 + }, "pdp11.js": { src: pkg.pdp11Files, dest: tmpPDP11 } - }, closureCompiler: { options: { @@ -293,7 +329,7 @@ module.exports = function(grunt) { compilerFile: "./bin/compiler.jar", // [OPTIONAL] set to true if you want to check if files were modified before starting compilation - checkModified: grunt.option("rebuild")? false : true, + checkModified: !grunt.option("rebuild"), // [OPTIONAL] Set Closure Compiler Directives here compilerOpts: { @@ -420,6 +456,33 @@ module.exports = function(grunt) { src: tmpPC8080, dest: "./versions/pc8080/" + pkg.version + "/pc8080-dbg.js" }, + "pdp10.js": { + TEMPcompilerOpts: { + create_source_map: "./tmp/pdpjs/" + pkg.version + "/pdp10.map", + define: ["\"APPVERSION='" + pkg.version + "'\"", + "\"SITEHOST='www.pcjs.org'\"", "COMPILED=true", "DEBUG=false", "DEBUGGER=false"], + output_wrapper: "\"(function(){%output%})();//# sourceMappingURL=/tmp/pdpjs/" + pkg.version + "/pdp10.map\"" + // output_wrapper: "\"(function(){%output%})();\"" + }, + // src: pkg.pdp10Files, + src: tmpPDP10, + dest: "./versions/pdpjs/" + pkg.version + "/pdp10.js" + }, + "pdp10-dbg.js": { + /* + * Technically, this is the one case we don't need to override the default 'define' settings, but maybe it's best to be explicit. + */ + TEMPcompilerOpts: { + create_source_map: "./tmp/pdpjs/" + pkg.version + "/pdp10-dbg.map", + define: ["\"APPVERSION='" + pkg.version + "'\"", + "\"SITEHOST='www.pcjs.org'\"", "COMPILED=true", "DEBUG=false", "DEBUGGER=true"], + output_wrapper: "\"(function(){%output%})();//# sourceMappingURL=/tmp/pdpjs/" + pkg.version + "/pdp10-dbg.map\"" + // output_wrapper: "\"(function(){%output%})();\"" + }, + // src: pkg.pdp10Files, + src: tmpPDP10, + dest: "./versions/pdpjs/" + pkg.version + "/pdp10-dbg.js" + }, "pdp11.js": { TEMPcompilerOpts: { create_source_map: "./tmp/pdpjs/" + pkg.version + "/pdp11.map", @@ -522,6 +585,26 @@ module.exports = function(grunt) { } } }, + "pdp10": { + files: [ + { + cwd: "modules/shared/templates/", + src: ["common.css", "common.xsl", "components.*", "document.css", "document.xsl", "machine.xsl", "manifest.xsl", "outline.xsl"], + dest: "versions/pdpjs/<%= pkg.version %>/", + expand: true + } + ], + options: { + process: function(content, srcPath) { + var s = content.replace(/()[^<]*(<\/xsl:variable>)/g, "$1pdp10$2"); + s = s.replace(/()[^<]*(<\/xsl:variable>)/g, "$1PDPjs$2"); + s = s.replace(/()[^<]*(<\/xsl:variable>)/g, "$1" + pkg.version + "$2"); + s = s.replace(/"[^"]*\/?(common.css|common.xsl|components.css|components.xsl|document.css|document.xsl)"/g, '"/versions/pdpjs/' + pkg.version + '/$1"'); + s = s.replace(/[ \t]*\/\*[^*][\s\S]*?\*\//g, "").replace(/[ \t]*[ \t]*\n?/g, ""); + return s; + } + } + }, "pdp11": { files: [ { @@ -667,7 +750,7 @@ module.exports = function(grunt) { grunt.loadTasks("modules/grunts/prepjs/tasks"); - grunt.registerTask("preCompiler", grunt.option("rebuild")? ["concat:tmp-c1pjs", "concat:tmp-pcx86", "concat:tmp-pc8080", "concat:tmp-pdp11"] : ["newer:concat:tmp-c1pjs", "newer:concat:tmp-pcx86", "newer:concat:tmp-pc8080", "newer:concat:tmp-pdp11"]); + grunt.registerTask("preCompiler", grunt.option("rebuild")? ["concat:tmp-c1pjs", "concat:tmp-pcx86", "concat:tmp-pc8080", "concat:tmp-pdp10", "concat:tmp-pdp11"] : ["newer:concat:tmp-c1pjs", "newer:concat:tmp-pcx86", "newer:concat:tmp-pc8080", "newer:concat:tmp-pdp10", "newer:concat:tmp-pdp11"]); grunt.registerTask("compile", ["preCompiler", "closureCompiler", "replace:fix-source-maps"]); diff --git a/docs/pcx86/examples/pcx86-dbg.js b/docs/pcx86/examples/pcx86-dbg.js index 8ed72740c..5aa39dcbc 100644 --- a/docs/pcx86/examples/pcx86-dbg.js +++ b/docs/pcx86/examples/pcx86-dbg.js @@ -718,7 +718,7 @@ case "^":d=f^e;break;case "|":d=f|e;break;case "&&":d=f&&e?1:0;break;case "||":d function Px(a,b,c){var d;if(b){b=Qx(a,b);for(var e=0,f=!1,g=b,h=[],k=[],m=b.split(/(\|\||&&|\||^|&|!=|==|>=|>>>|>>|>|<=|<<|<|-|\+|%|\/|\*)/);ec&&(d+=" '"+String.fromCharCode(c)+"'"));a.P((null!=b?b+": ":"")+d);return e}function Vx(a,b){if(b)return Tx(a,b,a.da[b]);var c=0;for(b in a.da)Tx(a,b,a.da[b]),c++;return 0=":6,">":6,"<=":6,"<":6,">>>":7,">>":7,"<<":7,"-":8,"+":8,"%":9,"/":9,"*":9}; +function Ux(a,b){var c;c=void 0===c?0:c;switch(a.ya){case 8:a=ja(b,3*c);break;case 10:a=b.toString();break;default:a=q(b,2*c)}return a}var Sx={"||":0,"&&":1,"|":2,"^":3,"&":4,"!=":5,"==":5,">=":6,">":6,"<=":6,"<":6,">>>":7,">>":7,"<<":7,"-":8,"+":8,"%":9,"/":9,"*":9}; function Wx(a){Nx.call(this,a);this.oa=4;this.ha=5;this.wa=1048575;this.K=Xx(this);this.Ga=Xx(this);this.Z=Xx(this);this.D=[];this.B=this.Y=this.M=[];Yx(this);this.ta=0;Zx(this);this.za={};$x(this,a.messages);this.Ca=a.commands;var b=this;window?void 0===window.pcx86&&(window.pcx86=function(a){return ay(b,a)}):void 0===global.pcx86&&(global.pcx86=function(a){return ay(b,a)})}ba(Wx,Nx);l=Wx.prototype; l.uc=function(a,b,c,d){this.ka=b;this.H=c;this.pa=a;this.Za=Ob(a,"FDC");this.Ha=Ob(a,"HDC");this.Tc=Ob(a,"FPU");this.G=Ob(a,"Mouse");(a=qd(a,"messages"))&&$x(this,a);this.ha=b.I>>2;this.wa=b.N;this.Ta=new Kd(this.H,7,"DBG");this.ma=by;80186<=this.H.ca&&(this.ma=by.slice(),this.ma[15]=cy,80286<=this.H.ca&&(this.ma[15]=dy,80386<=this.H.ca&&(this.oa=8)));rl(this,64,function(a){ey(d,d.H.sc,a[0])});rl(this,4,function(a){if(a=a[0]){var b=Rx(d,a);if(void 0===b)d.P("invalid selector: "+a);else if(a=fy(d, b,gy),d.P("dumpSel("+ka(a?a.U:b)+"): %"+q(a?a.Lb:null,d.ha)),a){var c,b=!1;if(a.type&4096)a.type&2048?(c="code"+(a.type&512?",readable":",execonly"),a.type&1024&&(c+=",conforming")):(c="data"+(a.type&512?",writable":",readonly"),a.type&1024&&(c+=",expdown")),a.type&256&&(c+=",accessed");else{var e=hy[a.type];e&&(c=e[0],b=e[1])}!c||a.jb&32768||(c+=",not present");d.P((b?"seg="+ka(a.ua&65535)+" off="+ka(a.Ka):"base="+q(a.ua,d.ha)+" limit="+iy(a.Ka))+" type="+r(a.type>>8)+" ("+c+") ext="+ka(a.ext&-65296)+ diff --git a/modules/pdp10/lib/bus.js b/modules/pdp10/lib/bus.js new file mode 100644 index 000000000..b27033749 --- /dev/null +++ b/modules/pdp10/lib/bus.js @@ -0,0 +1,717 @@ +/** + * @fileoverview Implements the PDP-10 Bus component. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +if (NODE) { + var Str = require("../../shared/lib/strlib"); + var Usr = require("../../shared/lib/usrlib"); + var Component = require("../../shared/lib/component"); + var State = require("../../shared/lib/state"); + var PDP10 = require("./defines"); + var MemoryPDP10 = require("./memory"); + var MessagesPDP10 = require("./messages"); +} + +/* + * Data types used by scanMemory() + */ + +/** + * This defines the BlockInfo bit fields used by scanMemory() when it creates the aBlocks array. + * + * @typedef {{ + * num: BitField, + * count: BitField, + * btmod: BitField, + * type: BitField + * }} + */ +var BlockInfoPDP10 = Usr.defineBitFields({num:20, count:8, btmod:1, type:3}); + +/** + * BusInfoPDP10 object definition (returned by scanMemory()) + * + * cbTotal: total bytes allocated + * cBlocks: total Memory blocks allocated + * aBlocks: array of allocated Memory block numbers + * + * @typedef {{ + * cbTotal: number, + * cBlocks: number, + * aBlocks: Array. + * }} + */ +var BusInfoPDP10; + +class BusPDP10 extends Component { + /** + * BusPDP10(parmsBus, cpu, dbg) + * + * The BusPDP10 component manages physical memory and I/O address spaces. + * + * The BusPDP10 component has no UI elements, so it does not require an init() handler, + * but it still inherits from the Component class and must be allocated like any + * other device component. It's currently allocated by the Computer's init() handler, + * which then calls the initBus() method of all the other components. + * + * For memory beyond the simple needs of the ROM and RAM components (ie, memory-mapped + * devices), the address space must still be allocated through the BusPDP10 component via + * addMemory(). If the component needs something more than simple read/write storage, + * it must provide a custom controller. + * + * @param {Object} parmsBus + * @param {CPUStatePDP10} cpu + * @param {DebuggerPDP10} dbg + */ + constructor(parmsBus, cpu, dbg) + { + super("Bus", parmsBus, MessagesPDP10.BUS); + + this.cpu = cpu; + this.dbg = dbg; + + /* + * Supported values for nBusWidth: 18 (default). This represents the maximum size of the bus for the + * life of the machine, regardless what memory management mode the CPU has enabled. + */ + this.nBusWidth = +parmsBus['busWidth'] || 18; + + /* + * Compute all BusPDP10 memory block parameters now, based on the width of the bus. + * + * Note that all PCjs machines divide their address space into blocks, using a block size appropriate for + * the machine's bus width. This allows us to efficiently allocate the entire address space, by reusing blocks + * as appropriate, and to define to different address behaviors on a block-granular level. + */ + this.addrTotal = 1 << this.nBusWidth; + this.nBusMask = (this.addrTotal - 1); + this.nBlockSize = 16384; + this.nBlockShift = Math.log2(this.nBlockSize); // ES6 ALERT (alternatively: Math.log(this.nBlockSize) / Math.LN2) + this.nBlockLen = this.nBlockSize >> 2; + this.nBlockLimit = this.nBlockSize - 1; + this.nBlockTotal = (this.addrTotal / this.nBlockSize) | 0; + this.nBlockMask = this.nBlockTotal - 1; + this.assert(this.nBlockMask <= BlockInfoPDP10.num.mask); + + /* + * Define all the properties to be initialized by initMemory() + */ + this.aBusBlocks = []; + + /* + * We're ready to allocate empty Memory blocks to span the entire physical address space. + */ + this.initMemory(); + + this.setReady(); + } + + /** + * initMemory() + * + * Allocate enough (empty) Memory blocks to span the entire physical address space. + * + * @this {BusPDP10} + */ + initMemory() + { + var block = new MemoryPDP10(this); + block.copyBreakpoints(this.dbg); + + this.aBusBlocks = new Array(this.nBlockTotal); + for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) { + this.aBusBlocks[iBlock] = block; + } + } + + /** + * reset() + * + * @this {BusPDP10} + */ + reset() + { + } + + /** + * getWidth() + * + * @this {BusPDP10} + * @return {number} + */ + getWidth() + { + return this.nBusWidth; + } + + /** + * powerUp(data, fRepower) + * + * @this {BusPDP10} + * @param {Object|null} data (always null because we supply no powerDown() handler) + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (!fRepower) { + if (!data) { + this.reset(); + } else { + if (!this.restore(data)) return false; + } + } + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * @this {BusPDP10} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + return fSave? this.save() : true; + } + + /** + * save() + * + * @this {BusPDP10} + * @return {Object|null} + */ + save() + { + var state = new State(this); + state.set(0, this.saveMemory()); + return state.data(); + } + + /** + * restore(data) + * + * @this {BusPDP10} + * @param {Object} data + * @return {boolean} true if restore successful, false if not + */ + restore(data) + { + return this.restoreMemory(data[0]); + } + + /** + * addMemory(addr, size, type) + * + * Adds new Memory blocks to the specified address range. Any Memory blocks previously + * added to that range must first be removed via removeMemory(); otherwise, you'll get + * an allocation conflict error. This helps prevent address calculation errors, redundant + * allocations, etc. + * + * We've relaxed some of the original requirements (ie, that addresses must start at a + * block-granular address, or that sizes must be equal to exactly one or more blocks), + * because machines with large block sizes can make it impossible to load certain ROMs at + * their required addresses. Every allocation still allocates a whole number of blocks. + * + * Even so, BusPDP10 memory management does NOT provide a general-purpose heap. Most memory + * allocations occur during machine initialization and never change. In particular, there + * is NO support for removing partial-block allocations. + * + * Each Memory block keeps track of a start address (addr) and length (used), indicating + * the used space within the block; any free space that precedes or follows that used space + * can be allocated later, by simply extending the beginning or ending of the previously used + * space. However, any holes that might have existed between the original allocation and an + * extension are subsumed by the extension. + * + * @this {BusPDP10} + * @param {number} addr is the starting physical address of the request + * @param {number} size of the request, in bytes + * @param {number} type is one of the MemoryPDP10.TYPE constants + * @return {boolean} true if successful, false if not + */ + addMemory(addr, size, type) + { + var addrNext = addr; + var sizeLeft = size; + var iBlock = addrNext >>> this.nBlockShift; + + while (sizeLeft > 0 && iBlock < this.aBusBlocks.length) { + + var block = this.aBusBlocks[iBlock]; + var addrBlock = iBlock * this.nBlockSize; + var sizeBlock = this.nBlockSize - (addrNext - addrBlock); + if (sizeBlock > sizeLeft) sizeBlock = sizeLeft; + + if (block && block.size) { + if (block.type == type) { + /* + * Where there is already a similar block with a non-zero size, we allow the allocation only if: + * + * 1) addrNext + sizeLeft <= block.addr (the request precedes the used portion of the current block), or + * 2) addrNext >= block.addr + block.used (the request follows the used portion of the current block) + */ + if (addrNext + sizeLeft <= block.addr) { + block.used += (block.addr - addrNext); + block.addr = addrNext; + return true; + } + if (addrNext >= block.addr + block.used) { + var sizeAvail = block.size - (addrNext - addrBlock); + if (sizeAvail > sizeLeft) sizeAvail = sizeLeft; + block.used = addrNext - block.addr + sizeAvail; + addrNext = addrBlock + this.nBlockSize; + sizeLeft -= sizeAvail; + iBlock++; + continue; + } + } + return this.reportError(BusPDP10.ERROR.RANGE_INUSE, addrNext, sizeLeft); + } + + var blockNew = new MemoryPDP10(this, addrNext, sizeBlock, this.nBlockSize, type); + blockNew.copyBreakpoints(this.dbg, block); + this.aBusBlocks[iBlock++] = blockNew; + + addrNext = addrBlock + this.nBlockSize; + sizeLeft -= sizeBlock; + } + + if (sizeLeft <= 0) { + this.status("Added " + (size >> 10) + "Kb " + MemoryPDP10.TYPE_NAMES[type] + " at " + Str.toOct(addr)); + return true; + } + + return this.reportError(BusPDP10.ERROR.RANGE_INVALID, addr, size); + } + + /** + * cleanMemory(addr, size) + * + * @this {BusPDP10} + * @param {number} addr + * @param {number} size + * @return {boolean} true if all blocks were clean, false if dirty; all blocks are cleaned in the process + */ + cleanMemory(addr, size) + { + var fClean = true; + var iBlock = addr >>> this.nBlockShift; + while (size > 0 && iBlock < this.aBusBlocks.length) { + if (this.aBusBlocks[iBlock].fDirty) { + this.aBusBlocks[iBlock].fDirty = fClean = false; + this.aBusBlocks[iBlock].fDirtyEver = true; + } + size -= this.nBlockSize; + iBlock++; + } + return fClean; + } + + /** + * zeroMemory(addr, size, pattern) + * + * @this {BusPDP10} + * @param {number} addr + * @param {number} size + * @param {number} [pattern] + */ + zeroMemory(addr, size, pattern) + { + var off = addr & this.nBlockLimit; + var iBlock = addr >>> this.nBlockShift; + while (size > 0 && iBlock < this.aBusBlocks.length) { + this.aBusBlocks[iBlock].zero(off, size, pattern); + size -= this.nBlockSize; + iBlock++; + off = 0; + } + } + + /** + * scanMemory(info, addr, size) + * + * Returns a BusInfoPDP10 object for the specified address range. + * + * @this {BusPDP10} + * @param {BusInfoPDP10} [info] previous BusInfoPDP10, if any + * @param {number} [addr] starting address of range (0 if none provided) + * @param {number} [size] size of range, in bytes (up to end of address space if none provided) + * @return {BusInfoPDP10} updated info (or new info if no previous info provided) + */ + scanMemory(info, addr, size) + { + if (addr == null) addr = 0; + if (size == null) size = (this.addrTotal - addr) | 0; + if (info == null) info = {cbTotal: 0, cBlocks: 0, aBlocks: []}; + + var iBlock = addr >>> this.nBlockShift; + var iBlockMax = ((addr + size - 1) >>> this.nBlockShift); + + info.cbTotal = 0; + info.cBlocks = 0; + while (iBlock <= iBlockMax) { + var block = this.aBusBlocks[iBlock]; + info.cbTotal += block.size; + if (block.size) { + info.aBlocks.push(/** @type {BlockInfoPDP10} */ (Usr.initBitFields(BlockInfoPDP10, iBlock, 0, 0, block.type))); + info.cBlocks++ + } + iBlock++; + } + return info; + } + + /** + * removeMemory(addr, size) + * + * Replaces every block in the specified address range with empty Memory blocks that ignore all reads/writes. + * + * TODO: Update the removeMemory() interface to reflect the relaxed requirements of the addMemory() interface. + * + * @this {BusPDP10} + * @param {number} addr + * @param {number} size + * @return {boolean} true if successful, false if not + */ + removeMemory(addr, size) + { + if (!(addr & this.nBlockLimit) && size && !(size & this.nBlockLimit)) { + var iBlock = addr >>> this.nBlockShift; + while (size > 0) { + var blockOld = this.aBusBlocks[iBlock]; + var blockNew = new MemoryPDP10(this, addr); + blockNew.copyBreakpoints(this.dbg, blockOld); + this.aBusBlocks[iBlock++] = blockNew; + addr = iBlock * this.nBlockSize; + size -= this.nBlockSize; + } + return true; + } + return this.reportError(BusPDP10.ERROR.RANGE_INVALID, addr, size); + } + + /** + * getMemoryBlocks(addr, size) + * + * @this {BusPDP10} + * @param {number} addr is the starting physical address + * @param {number} size of the request, in bytes + * @return {Array} of Memory blocks + */ + getMemoryBlocks(addr, size) + { + var aBlocks = []; + var iBlock = addr >>> this.nBlockShift; + while (size > 0 && iBlock < this.aBusBlocks.length) { + aBlocks.push(this.aBusBlocks[iBlock++]); + size -= this.nBlockSize; + } + return aBlocks; + } + + /** + * setMemoryBlocks(addr, size, aBlocks, type) + * + * If no type is specified, then specified address range uses all the provided blocks as-is; + * this form of setMemoryBlocks() is used for complete physical aliases. + * + * Otherwise, new blocks are allocated with the specified type; the underlying memory from the + * provided blocks is still used, but the new blocks may have different access to that memory. + * + * @this {BusPDP10} + * @param {number} addr is the starting physical address + * @param {number} size of the request, in bytes + * @param {Array} aBlocks as returned by getMemoryBlocks() + * @param {number} [type] is one of the MemoryPDP10.TYPE constants + */ + setMemoryBlocks(addr, size, aBlocks, type) + { + var i = 0; + var iBlock = addr >>> this.nBlockShift; + while (size > 0 && iBlock < this.aBusBlocks.length) { + var block = aBlocks[i++]; + this.assert(block); + if (!block) break; + if (type !== undefined) { + var blockNew = new MemoryPDP10(this, addr); + blockNew.clone(block, type, this.dbg); + block = blockNew; + } + this.aBusBlocks[iBlock++] = block; + size -= this.nBlockSize; + } + } + + /** + * getWord(addr) + * + * @this {BusPDP10} + * @param {number} addr is a physical address + * @return {number} word (16-bit) value at that address + */ + getWord(addr) + { + var off = addr & this.nBlockLimit; + var iBlock = (addr & this.nBusMask) >>> this.nBlockShift; + return this.aBusBlocks[iBlock].readWord(off, addr); + } + + /** + * setWord(addr, w) + * + * @this {BusPDP10} + * @param {number} addr is a physical address + * @param {number} w is the word (16-bit) value to write + */ + setWord(addr, w) + { + var off = addr & this.nBlockLimit; + var iBlock = (addr & this.nBusMask) >>> this.nBlockShift; + this.aBusBlocks[iBlock].writeWord(off, w, addr); + } + + /** + * getBlockDirect(addr) + * + * @this {BusPDP10} + * @param {number} addr is a physical address + * @return {MemoryPDP10} + */ + getBlockDirect(addr) + { + return this.aBusBlocks[(addr & this.nBusMask) >>> this.nBlockShift]; + } + + /** + * getWordDirect(addr) + * + * This is used for device I/O and Debugger physical memory requests, not the CPU. + * + * @this {BusPDP10} + * @param {number} addr is a physical address + * @return {number} word (16-bit) value at that address + */ + getWordDirect(addr) + { + var w; + var off = addr & this.nBlockLimit; + var block = this.getBlockDirect(addr); + w = block.readWordDirect(off, addr); + return w; + } + + /** + * setWordDirect(addr, w) + * + * This is used for device I/O and Debugger physical memory requests, not the CPU. + * + * @this {BusPDP10} + * @param {number} addr is a physical address + * @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe) + */ + setWordDirect(addr, w) + { + var off = addr & this.nBlockLimit; + var block = this.getBlockDirect(addr); + block.writeWordDirect(off, w & 0xffff, addr); + } + + /** + * addMemBreak(addr, fWrite) + * + * @this {BusPDP10} + * @param {number} addr + * @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint + */ + addMemBreak(addr, fWrite) + { + if (DEBUGGER) { + var iBlock = addr >>> this.nBlockShift; + this.aBusBlocks[iBlock].addBreakpoint(addr & this.nBlockLimit, fWrite); + } + } + + /** + * removeMemBreak(addr, fWrite) + * + * @this {BusPDP10} + * @param {number} addr + * @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint + */ + removeMemBreak(addr, fWrite) + { + if (DEBUGGER) { + var iBlock = addr >>> this.nBlockShift; + this.aBusBlocks[iBlock].removeBreakpoint(addr & this.nBlockLimit, fWrite); + } + } + + /** + * saveMemory(fAll) + * + * The only memory blocks we save are those marked as dirty, but most likely all of RAM will have been marked dirty, + * and even if our dirty-memory flags were as smart as our dirty-sector flags (ie, were set only when a write changed + * what was already there), it's unlikely that would reduce the number of RAM blocks we must save/restore. At least + * all the ROM blocks should be clean (except in the unlikely event that the Debugger was used to modify them). + * + * All dirty blocks will be stored in a single array, as pairs of block numbers and data arrays, like so: + * + * [iBlock0, [dw0, dw1, ...], iBlock1, [dw0, dw1, ...], ...] + * + * In a normal 4Kb block, there will be 1K DWORD values in the data array. Remember that each DWORD is a signed 32-bit + * integer (because they are formed using bit-wise operator rather than floating-point math operators), so don't be + * surprised to see negative numbers in the data. + * + * The above example assumes "uncompressed" data arrays. If we choose to use "compressed" data arrays, the data arrays + * will look like: + * + * [count0, dw0, count1, dw1, ...] + * + * where each count indicates how many times the following DWORD value occurs. A data array length less than 1K indicates + * that it's compressed, since we'll only store them in compressed form if they actually shrank, and we'll use State + * helper methods compress() and decompress() to create and expand the compressed data arrays. + * + * @this {BusPDP10} + * @param {boolean} [fAll] (true to save all non-ROM memory blocks, regardless of their dirty flags) + * @return {Array} a + */ + saveMemory(fAll) + { + var i = 0; + var a = []; + + for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) { + var block = this.aBusBlocks[iBlock]; + /* + * We have to check both fDirty and fDirtyEver, because we may have called cleanMemory() on some of + * the memory blocks (eg, video memory), and while cleanMemory() will clear a dirty block's fDirty flag, + * it also sets the dirty block's fDirtyEver flag, which is left set for the lifetime of the machine. + */ + if (fAll && block.type != MemoryPDP10.TYPE.ROM || block.fDirty || block.fDirtyEver) { + a[i++] = iBlock; + a[i++] = State.compress(block.save()); + } + } + + return a; + } + + /** + * restoreMemory(a) + * + * This restores the contents of all Memory blocks; called by CPUState.restore(). + * + * In theory, we ONLY have to save/restore block contents. Other block attributes, + * like the type, the memory controller (if any), and the active memory access functions, + * should already be restored, since every component (re)allocates all the memory blocks + * it was using when it's restored. And since the CPU is guaranteed to be the last + * component to be restored, all those blocks (and their attributes) should be in place now. + * + * See saveMemory() for more information on how the memory block contents are saved. + * + * @this {BusPDP10} + * @param {Array} a + * @return {boolean} true if successful, false if not + */ + restoreMemory(a) + { + var i; + for (i = 0; i < a.length - 1; i += 2) { + var iBlock = a[i]; + var adw = a[i+1]; + if (adw && adw.length < this.nBlockLen) { + adw = State.decompress(adw, this.nBlockLen); + } + var block = this.aBusBlocks[iBlock]; + if (!block || !block.restore(adw)) { + /* + * Either the block to restore hasn't been allocated, indicating a change in the machine + * configuration since it was last saved (the most likely explanation) or there's some internal + * inconsistency (eg, the block size is wrong). + */ + Component.error("Unable to restore memory block " + iBlock); + return false; + } + } + return true; + } + + /** + * getMemoryLimit(type) + * + * @this {BusPDP10} + * @param {number} type is one of the MemoryPDP10.TYPE constants + * @return {number} (the limiting address of the specified memory type, zero if none) + */ + getMemoryLimit(type) + { + var addr = 0; + for (var iBlock = 0; iBlock < this.aBusBlocks.length; iBlock++) { + var block = this.aBusBlocks[iBlock]; + if (block.type == type) { + addr = block.addr + block.used; + } + } + return addr; + } + + /** + * reportError(errNum, addr, size, fQuiet) + * + * @this {BusPDP10} + * @param {number} errNum + * @param {number} addr + * @param {number} size + * @param {boolean} [fQuiet] (true if any error should be quietly logged) + * @return {boolean} false + */ + reportError(errNum, addr, size, fQuiet) + { + var sError = "Memory block error (" + errNum + ": " + Str.toHex(addr) + "," + Str.toHex(size) + ")"; + if (fQuiet) { + if (this.dbg) { + this.dbg.message(sError); + } else { + this.log(sError); + } + } else { + Component.error(sError); + } + return false; + } +} + +BusPDP10.ERROR = { + RANGE_INUSE: 1, + RANGE_INVALID: 2 +}; + +if (NODE) module.exports = BusPDP10; diff --git a/modules/pdp10/lib/computer.js b/modules/pdp10/lib/computer.js new file mode 100644 index 000000000..b2e0072f3 --- /dev/null +++ b/modules/pdp10/lib/computer.js @@ -0,0 +1,1640 @@ +/** + * @fileoverview Implements the PDP-10 Computer component. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +if (NODE) { + var Str = require("../../shared/lib/strlib"); + var Usr = require("../../shared/lib/usrlib"); + var Web = require("../../shared/lib/weblib"); + var UserAPI = require("../../shared/lib/userapi"); + var ReportAPI = require("../../shared/lib/reportapi"); + var Component = require("../../shared/lib/component"); + var State = require("../../shared/lib/state"); + var PDP10 = require("./defines"); + var BusPDP10 = require("./bus"); + var MessagesPDP10 = require("./messages"); +} + +class ComputerPDP10 extends Component { + /** + * ComputerPDP10(parmsComputer, parmsMachine, fSuspended) + * + * The ComputerPDP10 component has no required (parmsComputer) properties, but it does + * support the following: + * + * autoPower: true to automatically power the computer (default), false to wait; + * false is honored only if a "power" button binding exists. + * + * busWidth: number of memory address lines (address bits) on the computer's "bus"; + * 20 is the minimum (and the default), which implies 8086/8088 real-mode addressing, + * while 24 is required for 80286 protected-mode addressing. This value is passed + * directly through to the Bus component; see that component for more details. + * + * resume: one of the ComputerPDP10.RESUME constants, which are as follows: + * '0' if resume disabled (default) + * '1' if enabled without prompting + * '2' if enabled with prompting + * '3' if enabled with prompting and auto-delete + * or a string containing the path of a predefined JSON-encoded state + * + * state: the path to JSON-encoded state file (see details regarding 'state' below) + * + * The parmsMachine object, if provided, may contain any of: + * + * autoMount: if set, this should override any 'autoMount' property in the FDC's + * parmsFDC object. + * + * autoPower: if set, this should override any 'autoPower' property in the ComputerPDP10's + * parmsComputer object. + * + * messages: if set, this should override any 'messages' property in the Debugger's + * parmsDbg object. + * + * state: if set, this should override any 'state' property in the ComputerPDP10's + * parmsComputer object. + * + * url: the location of the machine XML file + * + * If a predefined state is supplied AND it's successfully loaded, then resume behavior + * defaults to '1' (ie, resume enabled without prompting). + * + * This component insures that all components are ready before "powering" them. + * + * Different components become ready at different times, and initialization order (ie, + * the order the scripts are combined on the page) only partially determines readiness. + * This is because components like ROM and Video must finish loading their resource files + * before they are ready. Other components become ready after we call their initBus() + * function, because they have a Bus or CPU dependency, such as access to memory management + * functions. And other components, like CPU and Panel, are ready as soon as their + * constructor finishes. + * + * Once a component has indicated it's ready, we call its powerUp() notification + * function (if it has one--it's optional). We call the CPU's powerUp() function last, + * so that the CPU is assured that all other components are ready and "powered". + * + * @param {Object} parmsComputer + * @param {Object} [parmsMachine] + * @param {boolean} [fSuspended] + */ + constructor(parmsComputer, parmsMachine, fSuspended) + { + super("Computer", parmsComputer, MessagesPDP10.COMPUTER); + + this.flags.powered = false; + + this.parmsMachine = null; + this.setMachineParms(parmsMachine); + + this.fAutoPower = this.getMachineParm('autoPower', parmsComputer, Str.TYPES.BOOLEAN); + + /* + * nPowerChange is 0 while the power state is stable, 1 while power is transitioning + * to "on", and -1 while power is transitioning to "off". + */ + this.nPowerChange = 0; + + /* + * TODO: Deprecate 'buswidth' (it should have always used camelCase) + */ + this.nBusWidth = +parmsComputer['busWidth'] || +parmsComputer['buswidth']; + + this.sResumePath = this.sStatePath = null; + this.sStateData = null; + this.fStateData = false; // remembers if sStateData was loaded + this.fServerState = false; + this.stateComputer = this.stateFailSafe = null; + this.fInitialized = this.fReload = this.fRestoreError = false; + + this.url = /** @type {string} */ (this.getMachineParm('url') || ""); + + /* + * Generate a random number x (where 0 <= x < 1), add 0.1 so that it's guaranteed to be + * non-zero, convert to base 36, and chop off the leading digit and "decimal" point. + */ + this.sMachineID = (Math.random() + 0.1).toString(36).substr(2,12); + this.sUserID = this.queryUserID(); + + /* + * Find the appropriate CPU (and Debugger and Control Panel, if any). + * + * CLOSURE COMPILER TIP: To override the type of a right-hand expression (as we need to do here, + * where we know getComponentByType() will only return an CPUState object or null), wrap the expression + * in parentheses. I never knew this until I stumbled across it in "Closure: The Definitive Guide". + */ + this.cpu = /** @type {CPUStatePDP10} */ (Component.getComponentByType("CPU", this.id)); + if (!this.cpu) { + Component.error("Unable to find CPU component"); + return; + } + this.dbg = /** @type {DebuggerPDP10} */ (Component.getComponentByType("Debugger", this.id)); + + /* + * Initialize the Bus component + */ + this.bus = new BusPDP10({'id': this.idMachine + '.bus', 'busWidth': this.nBusWidth}, this.cpu, this.dbg); + + /* + * Iterate through all the components and connect them to the Control Panel, if any + */ + var iComponent, component; + var aComponents = Component.getComponents(this.id); + this.panel = /** @type {PanelPDP10} */ (Component.getComponentByType("Panel", this.id)); + + if (this.panel && this.panel.controlPrint) { + for (iComponent = 0; iComponent < aComponents.length; iComponent++) { + component = aComponents[iComponent]; + /* + * I can think of many "cleaner" ways for the Control Panel component to pass its + * notice(), println(), etc, overrides on to all the other components, but it's just + * too darn convenient to slam those overrides into the components directly. + */ + component.notice = this.panel.notice; + component.println = this.panel.println; + component.controlPrint = this.panel.controlPrint; + } + } + + this.println(PDP10.APPNAME + " v" + PDP10.APPVERSION + "\n" + COPYRIGHT + "\n" + LICENSE); + + /* + * Iterate through all the components again and call their initBus() handler, if any + */ + for (iComponent = 0; iComponent < aComponents.length; iComponent++) { + component = aComponents[iComponent]; + if (component.initBus) component.initBus(this, this.bus, this.cpu, this.dbg); + } + + var sStatePath = null; + var sResume = /** @type {string} */ (this.getMachineParm('resume', parmsComputer)); + if (sResume !== undefined) { + /* + * Decide whether the 'resume' property is a number or the path of a state file to resume. + */ + if (sResume.length > 1) { + sStatePath = this.sResumePath = sResume; + } else { + this.resume = parseInt(sResume, 10); + } + } + + /* + * The Computer 'state' property allows a state file to be specified independent of the 'resume' feature; + * previously, you could only use 'resume' to load a state file -- which we still support, but loading a state + * file that way prevents the machine's state from being saved, since we always resume from the 'resume' file. + * + * The other wrinkle is on the restore side: we need to IGNORE the 'state' property if a saved state now exists. + * So we have to peek at localStorage, and unfortunately, the only way to "peek" is to actually load the data, + * but we're not ready to use it yet, so powerUp() has been changed to use any existing stateComputer that we've + * already loaded. + * + * However, there's now a wrinkle to the wrinkle: if a 'state' parameter has been passed via the URL, then that + * OVERRIDES everything; it overrides any 'state' Computer parameter AND it disables resume of any saved state in + * localStorage (in other words, it prevents fAllowResume from being true, and forcing resume off). + */ + var fAllowResume; + var sState = this.getMachineParm('state') || (fAllowResume = true) && parmsComputer['state']; + + if (sState) { + this.sStatePath = sStatePath = sState; + if (!fAllowResume) { + this.fServerState = true; + this.resume = ComputerPDP10.RESUME_NONE; + } + if (this.resume) { + this.stateComputer = new State(this, PDP10.APPVERSION); + if (this.stateComputer.load()) { + sStatePath = null; + } else { + delete this.stateComputer; + } + } + } + + /* + * If sStatePath is set, we must use it. But if there's no sStatePath AND resume is set, + * then we have the option of resuming from a server-side state, assuming a valid USERID. + */ + if (!sStatePath && this.resume) { + sStatePath = this.getServerStatePath(); + if (sStatePath) this.fServerState = true; + } + + if (!sStatePath) { + this.setReady(); + } else { + var cmp = this; + Web.getResource(sStatePath, null, true, function doneStateLoad(sURL, sResource, nErrorCode) { + cmp.finishStateLoad(sURL, sResource, nErrorCode); + }); + } + + if (!this.bindings["power"]) this.fAutoPower = true; + + /* + * Power on the computer, giving every component the opportunity to reset or restore itself. + */ + if (!fSuspended && this.fAutoPower) this.wait(this.powerOn); + } + + /** + * getMachineID() + * + * @return {string} + */ + getMachineID() + { + return this.sMachineID; + } + + /** + * setMachineParms(parmsMachine) + * + * If no explicit machine parms were provided, then we check for 'parms' in the bundled resources (if any). + * + * @param {Object} [parmsMachine] + */ + setMachineParms(parmsMachine) + { + if (!parmsMachine) { + var sParms; + if (typeof resources == 'object' && (sParms = resources['parms'])) { + try { + parmsMachine = /** @type {Object} */ (eval("(" + sParms + ")")); + } catch(e) { + Component.error(e.message + " (" + sParms + ")"); + } + } + } + this.parmsMachine = parmsMachine; + } + + /** + * getMachineParm(sParm, parmsComponent, type, defaultValue) + * + * If the machine parameter doesn't exist, we check for a matching component parameter + * (if parmsComponent is provided), and failing that, we check the bundled resources (if any). + * + * At the moment, the only bundled resource request we expect to encounter is 'state'; if it exists, + * then we return 'state' back to the caller (ie, the name of the resource), so that the caller will + * then attempt to load the 'state' resource to obtain the actual state. + * + * TODO: It would be nice if we could tell the Closure Compiler that when a specific type parameter + * (eg, Str.TYPES.NUMBER) is used, the return value will be that type; unfortunately, every caller + * must coerce their own return value. + * + * @param {string} sParm + * @param {Object|null} [parmsComponent] + * @param {number} [type] (from Str.TYPES) + * @param {*} [defaultValue] + * @return {*} + */ + getMachineParm(sParm, parmsComponent, type, defaultValue) + { + /* + * When checking parmsURL, the check is allowed be a bit looser, because URL parameters are + * user-supplied, whereas most other parameters are developer-supplied. Granted, a developer + * may also be sloppy and neglect to use correct case (eg, 'automount' instead of 'autoMount'), + * but there are limits to my paranoia. + */ + var sParmLC = sParm.toLowerCase(); + var value = Web.getURLParm(sParm) || Web.getURLParm(sParmLC); + if (value === undefined && this.parmsMachine) value = this.parmsMachine[sParm]; + if (value === undefined && parmsComponent) value = parmsComponent[sParm]; + if (value === undefined && typeof resources == 'object' && resources[sParm]) value = sParm; + if (value === undefined) value = defaultValue; + if (typeof value == "string" && type) { + switch(type) { + case Str.TYPES.NUMBER: + value = +value; + if (isNaN(/** @type {number} */(value))) value = defaultValue || 0; + break; + case Str.TYPES.BOOLEAN: + value = (value == "true"); + break; + } + } + return value; + } + + /** + * saveMachineParms() + * + * @return {string|null} + */ + saveMachineParms() + { + return this.parmsMachine? JSON.stringify(this.parmsMachine) : null; + } + + /** + * getUserID() + * + * @return {string} + */ + getUserID() + { + return this.sUserID || ""; + } + + /** + * finishStateLoad(sURL, sStateData, nErrorCode) + * + * @this {ComputerPDP10} + * @param {string} sURL + * @param {string} sStateData + * @param {number} nErrorCode + */ + finishStateLoad(sURL, sStateData, nErrorCode) + { + if (!nErrorCode) { + this.sStateData = sStateData; + this.fStateData = true; + if (DEBUG && this.messageEnabled()) { + this.printMessage("loaded state file " + sURL.replace(this.sUserID || "xxx", "xxx")); + } + } else { + this.sResumePath = null; + this.fServerState = false; + this.notice('Unable to load machine state from server (error ' + nErrorCode + (sStateData? ': ' + Str.trim(sStateData) : '') + ')'); + } + this.setReady(); + } + + /** + * wait(fn, parms) + * + * wait() waits until every component is ready (including ourselves, the last component we check), then calls the + * specified Computer method. + * + * TODO: The Closure Compiler makes it difficult for us to define a function type for "fn" that works in all cases; + * sometimes we want to pass a function that takes only a "number", and other times we want to pass a function that + * takes only an "Array" (the type will mirror that of the "parms" parameter). However, the Closure Compiler insists + * that both functions must be declared as accepting both types of parameters. So once again, we must use an untyped + * function declaration, instead of something stricter like: + * + * param {function(this:Computer, (number|Array|undefined)): undefined} fn + * + * @this {ComputerPDP10} + * @param {function(...)} fn + * @param {number|Array} [parms] optional parameters + */ + wait(fn, parms) + { + var computer = this; + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent <= aComponents.length; iComponent++) { + var component = (iComponent < aComponents.length ? aComponents[iComponent] : this); + if (!component.isReady()) { + component.isReady(function onComponentReady() { + computer.wait(fn, parms); + }); + return; + } + } + if (DEBUG && this.messageEnabled()) this.printMessage("ComputerPDP10.wait(ready)"); + //noinspection JSUnresolvedFunction + fn.call(this, parms); + } + + /** + * validateState(stateComputer) + * + * NOTE: We clear() stateValidate only when there's no stateComputer. + * + * @this {ComputerPDP10} + * @param {State|null} [stateComputer] + * @return {boolean} true if state passes validation, false if not + */ + validateState(stateComputer) + { + var fValid = true; + var stateValidate = new State(this, PDP10.APPVERSION, ComputerPDP10.STATE_VALIDATE); + if (stateValidate.load() && stateValidate.parse()) { + var sTimestampValidate = stateValidate.get(ComputerPDP10.STATE_TIMESTAMP); + var sTimestampComputer = stateComputer? stateComputer.get(ComputerPDP10.STATE_TIMESTAMP) : "unknown"; + if (sTimestampValidate != sTimestampComputer) { + this.notice("Machine state may be out-of-date\n(" + sTimestampValidate + " vs. " + sTimestampComputer + ")\nCheck your browser's local storage limits"); + fValid = false; + if (!stateComputer) stateValidate.clear(); + } else { + if (DEBUG && this.messageEnabled()) { + this.printMessage("Last state: " + sTimestampComputer + " (validate: " + sTimestampValidate + ")"); + } + } + } + return fValid; + } + + /** + * powerOn(resume) + * + * Power every component "up", applying any previously available state information. + * + * @this {ComputerPDP10} + * @param {number} [resume] is a valid RESUME value; default is this.resume + */ + powerOn(resume) + { + if (resume === undefined) { + resume = this.resume || (this.sStateData? ComputerPDP10.RESUME_AUTO : ComputerPDP10.RESUME_NONE); + } + + if (DEBUG && this.messageEnabled()) { + this.printMessage("ComputerPDP10.powerOn(" + (resume == ComputerPDP10.RESUME_REPOWER ? "repower" : (resume ? "resume" : "")) + ")"); + } + + if (this.nPowerChange) { + return; + } + this.nPowerChange++; + + var fRepower = false; + var fRestore = false; + this.fRestoreError = false; + var stateComputer = this.stateComputer || new State(this, PDP10.APPVERSION); + + if (resume == ComputerPDP10.RESUME_REPOWER) { + fRepower = true; + } + else if (resume > ComputerPDP10.RESUME_NONE) { + if (stateComputer.load(this.sStateData)) { + /* + * Since we're resuming something (either a predefined state or a state from localStorage), let's + * create a "failsafe" checkpoint in localStorage, and destroy it at the end of a successful powerOn(). + * Which means, of course, that if a previous "failsafe" checkpoint already exists, something bad + * may have happened the last time around. + */ + this.stateFailSafe = new State(this, PDP10.APPVERSION, ComputerPDP10.STATE_FAILSAFE); + if (this.stateFailSafe.load()) { + this.powerReport(stateComputer); + /* + * We already know resume is something other than RESUME_NONE, so we'll go ahead and bump it + * all the way to RESUME_PROMPT, so that the user will be prompted, and if the user declines to + * restore, the state will be removed. + */ + resume = ComputerPDP10.RESUME_PROMPT; + /* + * To ensure that the set() below succeeds, we need to call unload(), otherwise it may fail + * with a "read only" error (eg, "TypeError: Cannot assign to read only property 'timestamp'"). + */ + this.stateFailSafe.unload(); + } + + this.stateFailSafe.set(ComputerPDP10.STATE_TIMESTAMP, Usr.getTimestamp()); + this.stateFailSafe.store(); + + var fValidate = this.resume && !this.fServerState; + if (resume == ComputerPDP10.RESUME_AUTO || Component.confirmUser("Click OK to restore the previous " + PDP10.APPNAME + " machine state, or CANCEL to reset the machine.")) { + fRestore = stateComputer.parse(); + if (fRestore) { + var sCode = /** @type {string} */ (stateComputer.get(UserAPI.RES.CODE)); + var sData = /** @type {string} */ (stateComputer.get(UserAPI.RES.DATA)); + if (sCode) { + if (sCode == UserAPI.CODE.OK) { + stateComputer.load(sData); + } else { + /* + * A missing (or not yet created) state file is no cause for alarm, but other errors might be + */ + if (sCode == UserAPI.CODE.FAIL && sData != UserAPI.FAIL.NOSTATE) { + this.notice("Error: " + sData); + if (sData == UserAPI.FAIL.VERIFY) this.resetUserID(); + } else { + this.println(sCode + ": " + sData); + } + /* + * Try falling back to the state that we should have saved in localStorage, as a backup to the + * server-side state. + */ + stateComputer.unload(); // discard the invalid server-side state first + if (stateComputer.load()) { + fRestore = stateComputer.parse(); + fValidate = true; + } else { + fRestore = false; // hmmm, there was nothing in localStorage either + } + } + } + } + /* + * If the load/parse was successful, and it was from localStorage (not sStateData), + * then we should to try verify that localStorage snapshot is current. One reason it may + * NOT be current is if localStorage was full and we got a quota error during the last + * powerOff(). + */ + if (fValidate) this.validateState(fRestore? stateComputer : null); + } else { + /* + * RESUME_PROMPT indicates we should delete the state if they clicked Cancel to confirm() above. + */ + if (resume == ComputerPDP10.RESUME_PROMPT) stateComputer.clear(); + } + } else { + /* + * If there's no state, then there should also be no validation timestamp; if there is, then once again, + * we're probably dealing with a quota error. + */ + this.validateState(); + } + delete this.sStateData; + delete this.stateComputer; + } + + /* + * Start powering all components, including any data they may need to restore their state; + * we restore power to the CPU last. + */ + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (component !== this && component != this.cpu) { + fRestore = this.powerRestore(component, stateComputer, fRepower, fRestore); + } + } + + /* + * Assuming this is not a repower, we must perform another wait, because some components may + * have marked themselves as "not ready" again (eg, the FDC component, if the restore forced it + * to mount one or more additional disk images). + */ + var aParms = [stateComputer, resume, fRestore]; + + if (resume != ComputerPDP10.RESUME_REPOWER) { + this.wait(this.donePowerOn, aParms); + return; + } + this.donePowerOn(aParms); + } + + /** + * powerRestore(component, stateComputer, fRepower, fRestore) + * + * @this {ComputerPDP10} + * @param {Component} component + * @param {State} stateComputer + * @param {boolean} fRepower + * @param {boolean} fRestore + * @return {boolean} true if restore should continue, false if not + */ + powerRestore(component, stateComputer, fRepower, fRestore) + { + if (!component.flags.powered) { + + /* + * TODO: If all components called super.powerUp(), the powered flag would be set automatically. + */ + this.assert(component.powerUp); + component.flags.powered = true; + + var data = null; + + try { + if (fRestore) { + data = stateComputer.get(component.id); + if (!data) { + /* + * This is a hack that makes it possible for a machine whose ID has been + * supplemented with a suffix (a single letter or digit) to find object IDs + * in states created from a machine without the suffix. + * + * For example, if a state file was created from a machine with ID "ibm5160" + * but the current machine is "ibm5160a", this attempts a second lookup with + * "ibm5160", enabling us to find objects that match the original machine ID + * (eg, "ibm5160.romEGA"). + */ + data = stateComputer.get(component.id.replace(/[a-z0-9]\./i, '.')); + } + } + + /* + * State.get() will return whatever was originally passed to State.set() (eg, an + * Object or a string), but components are supposed to store only Objects, so if a + * string comes back, something went wrong. By explicitly eliminating "string" data, + * the Closure Compiler stops complaining that we might be passing strings to our + * powerUp() functions (even though we know we're not). + * + * TODO: Determine if there's some way to coerce the Closure Compiler into treating + * data as Object or null, without having to include this runtime check. An assert + * would be a good idea, but this is overkill. + */ + if (typeof data === "string") data = null; + + /* + * If computer is null, this is simply a repower notification, which most components + * don't do anything with. Exceptions include: CPU (since it may be halted) and Video + * (since its screen may be "turned off"). + */ + if (!component.powerUp(data, fRepower) && data) { + + Component.error("Unable to restore state for " + component.type); + /* + * If this is a resume error for a machine that also has a predefined state + * AND we're not restoring from that state, then throw away the current state, + * prevent any new state from being created, and then force a reload, which will + * hopefully restore us to the functioning predefined state. + * + * TODO: Considering doing this in ALL cases, not just in situations where a + * 'state' exists but we're not actually resuming from it. + */ + if (this.sStatePath && !this.fStateData) { + stateComputer.clear(); + this.resume = ComputerPDP10.RESUME_NONE; + Web.reloadPage(); + } else { + /* + * In all other cases, we set fRestoreError, which should trigger a call to + * powerReport() and then delete the offending state. + */ + this.fRestoreError = true; + } + /* + * Any failure triggers an automatic to call powerUp() again, without any state, + * in the hopes that the component can recover by performing a reset. + */ + component.powerUp(null); + /* + * We also disable the rest of the restore operation, because it's not clear + * the remaining state information can be trusted; the machine is already in an + * inconsistent state, so we're not likely to make things worse, and the only + * alternative (starting over and performing a state-less reset) isn't likely to make + * the user any happier. But, we'll see... we need some experience with the code. + */ + fRestore = false; + } + + if (!fRepower && component.comment) { + var asComments = component.comment.split("|"); + for (var i = 0; i < asComments.length; i++) { + component.status(asComments[i]); + } + } + } + catch (err) { + Component.error("Error restoring state for " + component.type + " (" + err.message + ")"); + } + } + return fRestore; + } + + /** + * donePowerOn(aParms) + * + * This is nothing more than a continuation of powerOn(), giving us the option of calling wait() one more time. + * + * @this {ComputerPDP10} + * @param {Array} aParms containing [stateComputer, resume, fRestore] + */ + donePowerOn(aParms) + { + var stateComputer = aParms[0]; + var fRepower = (aParms[1] < 0); + var fRestore = aParms[2]; + + if (DEBUG && this.flags.powered && this.messageEnabled()) { + this.printMessage("ComputerPDP10.donePowerOn(): redundant"); + } + + this.fInitialized = true; + this.flags.powered = true; + var controlPower = this.bindings["power"]; + if (controlPower) controlPower.textContent = "Shutdown"; + + /* + * Once we get to this point, we're guaranteed that all components are ready, so it's safe to power the CPU; + * the CPU should begin executing immediately, unless a debugger is attached. + */ + if (this.cpu) { + /* + * TODO: Do we not care about the return value here? (ie, is checking fRestoreError sufficient)? + */ + this.powerRestore(this.cpu, stateComputer, fRepower, fRestore); + this.updateDisplays(-2); + this.cpu.autoStart(); + } + + /* + * If the state was bad, offer to report it and then delete it. Deleting may be moot, since invariably a new + * state will be created on powerOff() before the next powerOn(), but it seems like good paranoia all the same. + */ + if (this.fRestoreError) { + this.powerReport(stateComputer); + stateComputer.clear(); + } + + if (!fRepower && this.stateFailSafe) { + this.stateFailSafe.clear(); + delete this.stateFailSafe; + } + + this.nPowerChange = 0; + } + + /** + * checkPower() + * + * @this {ComputerPDP10} + * @return {boolean} true if the computer is fully powered, false otherwise + */ + checkPower() + { + if (this.flags.powered) return true; + + var component = null, iComponent; + var aComponents = Component.getComponents(this.id); + for (iComponent = 0; iComponent < aComponents.length; iComponent++) { + component = aComponents[iComponent]; + if (component !== this && !component.flags.ready) break; + } + if (iComponent == aComponents.length) { + for (iComponent = 0; iComponent < aComponents.length; iComponent++) { + component = aComponents[iComponent]; + if (component !== this && !component.flags.powered) break; + } + } + if (iComponent == aComponents.length) component = this; + var s = "The " + component.type + " component (" + component.id + ") is not " + (!component.flags.ready? "ready yet" + (component.fnReady? " (waiting for notification)" : "") : "powered yet") + "."; + Component.alertUser(s); + return false; + } + + /** + * powerReport(stateComputer) + * + * @this {ComputerPDP10} + * @param {State} stateComputer + */ + powerReport(stateComputer) + { + if (Component.confirmUser("There may be a problem with your " + PDP10.APPNAME + " machine.\n\nTo help us diagnose it, click OK to send this " + PDP10.APPNAME + " machine state to http://" + SITEHOST + ".")) { + Web.sendReport(PDP10.APPNAME, PDP10.APPVERSION, this.url, this.getUserID(), ReportAPI.TYPE.BUG, stateComputer.toString()); + } + } + + /** + * powerOff(fSave, fShutdown) + * + * Power every component "down" and optionally save the machine state. + * + * There's one scenario that powerOff() isn't currently able to deal with very effectively: what to do when + * the user switches away while it's still being restored, causing Disk getResource() calls to fail. The + * Disk component calls notify() when that happens -- see Disk.mount() -- but the FDC and HDC controllers don't + * notify *us* of those problems, so Computer assumes that the restore was completely successful, when in fact + * it was only partially successful. + * + * Then we immediately arrive here to perform a save, following that incomplete restore. It would be wrong to + * deal with that incomplete restore by setting fRestoreError, because we don't want to trigger a powerReport() + * and the deletion of the previous state, because the state itself was presumably OK. Unfortunately, the new + * state we now save will no longer include manually mounted disk images whose remounts were interrupted, so future + * restores won't remount them either. + * + * We could perhaps solve this by having the Disk component notify us in those situations, set a new flag + * (fRestoreIncomplete?), and set fSave to false if that's ever set. Be careful though: when fSave is false, + * that means MORE than not saving; it also means deleting any previous state, which is NOT what you'd want to + * do in a "fRestoreIncomplete" situation. Also, we have to worry about Disk operations that fail for other reasons, + * making sure those failures don't interfere with the save process in the same way. + * + * As it stands, the worst that happens is any manually mounted disk images might have to be manually remounted, + * which doesn't seem like a huge problem. + * + * @this {ComputerPDP10} + * @param {boolean} [fSave] is true to request a saved state + * @param {boolean} [fShutdown] is true if the machine is being shut down + * @return {string|null} string representing the saved state (or null if error) + */ + powerOff(fSave, fShutdown) + { + var data; + var sState = "none"; + + if (DEBUG && this.messageEnabled()) { + this.printMessage("ComputerPDP10.powerOff(" + (fSave ? "save" : "nosave") + (fShutdown ? ",shutdown" : "") + ")"); + } + + if (this.nPowerChange) { + return null; + } + this.nPowerChange--; + + var stateComputer = new State(this, PDP10.APPVERSION); + var stateValidate = new State(this, PDP10.APPVERSION, ComputerPDP10.STATE_VALIDATE); + + var sTimestamp = Usr.getTimestamp(); + stateValidate.set(ComputerPDP10.STATE_TIMESTAMP, sTimestamp); + stateComputer.set(ComputerPDP10.STATE_TIMESTAMP, sTimestamp); + stateComputer.set(ComputerPDP10.STATE_VERSION, APPVERSION); + stateComputer.set(ComputerPDP10.STATE_HOSTURL, Web.getHostURL()); + stateComputer.set(ComputerPDP10.STATE_BROWSER, Web.getUserAgent()); + + /* + * Always power the CPU "down" first, just to help insure it doesn't ask other components to do anything + * after they're no longer ready. + */ + if (this.cpu && this.cpu.powerDown) { + if (fShutdown) { + if (fSave) this.cpu.flags.autoStart = this.cpu.flags.running; + this.cpu.stopCPU(); + } + data = this.cpu.powerDown(fSave, fShutdown); + if (typeof data === "object") stateComputer.set(this.cpu.id, data); + if (fShutdown) { + this.cpu.flags.powered = false; + if (data === false) sState = null; + } + } + + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (component.flags.powered) { + if (component.powerDown) { + data = component.powerDown(fSave, fShutdown); + if (typeof data === "object") stateComputer.set(component.id, data); + } + if (fShutdown) { + component.flags.powered = false; + if (data === false) sState = null; + } + } + } + + if (sState) { + if (fShutdown) { + var fClear = false; + var fClearAll = false; + if (fSave) { + if (this.sUserID) { + this.saveServerState(this.sUserID, stateComputer.toString()); + } + if (!stateValidate.store() || !stateComputer.store()) { + sState = null; + /* + * New behavior as of v1.13.2: if it appears that localStorage is full, we blow it ALL away. + * Dedicated server-side storage is the only way we'll ever be able to reliably preserve a + * particular machine's state. Historically, attempting to limp along with whatever localStorage + * is left just generates the same useless and annoying warnings over and over. + */ + fClear = fClearAll = true; + } + } + else { + /* + * I used to ALWAYS clear (ie, delete) any associated computer state, but now I do this only if the + * current machine is "resumable", because there are situations where I have two configurations + * for the same machine -- one resumable and one not -- and I don't want the latter throwing away the + * state of the former. + * + * So this code is here now strictly for callers to delete the state of a "resumable" machine, not as + * some paranoid clean-up operation. + * + * An undocumented feature of this operation is that if your configuration uses the special 'resume="3"' + * value, and you click the "Reset" button, and then you click OK to reset the everything, this will + * actually reset EVERYTHING (ie, all localStorage for ALL configs will be reclaimed). + */ + if (this.resume) { + fClear = true; + fClearAll = (this.resume == ComputerPDP10.RESUME_DELETE); + } + } + if (fClear) { + stateComputer.clear(fClearAll); + } + } else { + sState = stateComputer.toString(); + } + } + + if (fShutdown) { + this.flags.powered = false; + var controlPower = this.bindings["power"]; + if (controlPower) controlPower.textContent = "Power"; + } + + this.nPowerChange = 0; + + return sState; + } + + /** + * reset() + * + * Notify all (other) components with a reset() method that the Computer is being reset. + * + * NOTE: We'd like to reset the Bus first (due to the importance of the A20 line), but since we + * allocated the Bus object ourselves, after all the other components were allocated, it ends + * up near the end of Component's list of components. Hence the special case for this.bus below. + * + * Ditto for the CPU, in part because if the Front Panel resets before the CPU, it will end up + * snapping/displaying the PC as of the last instruction executed, before the CPU resets the PC, + * causing the Front Panel to display a stale address when we call updateDisplays() at the end. + * + * @this {ComputerPDP10} + */ + reset() + { + this.flags.reset = true; + if (this.bus && this.bus.reset) { + this.printMessage("Resetting " + this.bus.type); + this.bus.reset(); + } + if (this.cpu && this.cpu.reset) { + this.printMessage("Resetting " + this.cpu.type); + this.cpu.reset(); + } + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (component !== this && component !== this.bus && component !== this.cpu && component.reset) { + this.printMessage("Resetting " + component.type); + component.reset(); + } + } + this.flags.reset = false; + this.updateDisplays(-1); + } + + /** + * start(ms, nCycles) + * + * Notify all (other) components with a start() method that the CPU has started. + * + * Note that we're called by startCPU(), which is why we exclude the CPU component, + * as well as ourselves. + * + * @this {ComputerPDP10} + * @param {number} ms + * @param {number} nCycles + */ + start(ms, nCycles) + { + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (component.type == "CPU" || component === this) continue; + if (component.start) { + component.start(ms, nCycles); + } + } + this.updateDisplays(-1); + } + + /** + * stop(ms, nCycles) + * + * Notify all (other) components with a stop() method that the CPU has stopped. + * + * Note that we're called by stopCPU(), which is why we exclude the CPU component, + * as well as ourselves. + * + * @this {ComputerPDP10} + * @param {number} ms + * @param {number} nCycles + */ + stop(ms, nCycles) + { + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (component.type == "CPU" || component === this) continue; + if (component.stop) { + component.stop(ms, nCycles); + } + } + this.updateDisplays(-1); + } + + /** + * updateDisplays(nUpdate) + * + * TODO: Notify all components with an updateDisplay() method that the computer's state has changed (not + * just the hard-coded ones below). + * + * If any DOM controls were bound to the CPU, then we need to call its updateDisplay() handler; if there are no + * such bindings, then cpu.updateDisplay() does nothing. + * + * Similarly, if there's a Panel, then we need to call its updateDisplay() handler, in case it created its own canvas + * and implemented its own register display (eg, dumpRegisters()); if not, then panel.updateDisplay() also does nothing. + * + * In practice, there will *either* be a Panel with a custom canvas *or* a set of DOM controls bound to the CPU *or* + * neither. In theory, there could be BOTH, but that would be unusual. + * + * TODO: Consider alternate approaches to these largely register-oriented display updates. Ordinarily, we like to + * separate logic from presentation, and currently the CPUState contains both, since it's the component that intimately + * knows the names, number, sizes, etc, of all the active registers. The Panel component is the logical candidate, + * but Panel is an optional component; it's often the case that only machines that include the Debugger also include + * Panel. + * + * @this {ComputerPDP10} + * @param {number} [nUpdate] (1 for periodic, -1 for forced, 0 or undefined otherwise) + */ + updateDisplays(nUpdate) + { + /* + * nUpdate is generally set to -1 whenever the CPU is transitioning to/from a running state, in which case + * cpu.updateDisplay() will definitely want to hide/show register contents; however, at other times, when the + * CPU is running, constantly updating the DOM controls too frequently can adversely impact overall performance. + * + * nUpdate will also be -1 whenever the Debugger has modified the state of the machine, implying that we're + * not sure what, if anything, actually changed. + */ + if (this.cpu) this.cpu.updateDisplay(nUpdate || 0); + if (this.panel) this.panel.updateDisplay(nUpdate || 0); + } + + /** + * setBinding(sType, sBinding, control, sValue) + * + * @this {ComputerPDP10} + * @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc) + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sType, sBinding, control, sValue) + { + var computer = this; + + switch (sBinding) { + case "power": + this.bindings[sBinding] = control; + control.onclick = function onClickPower() { + computer.onPower(); + }; + return true; + + case "reset": + this.bindings[sBinding] = control; + control.onclick = function onClickReset() { + computer.onReset(); + }; + return true; + + /* + * Technically, this binding should now be called "saveState", to clearly distinguish it from + * the "Save Machine" control that's normally bound to the savePC() function in save.js. Saving + * an entire machine includes everything needed to start/restore the machine; eg, the machine + * XML configuration file(s) *and* the JSON-encoded machine state. + */ + case "save": + /* + * Since this feature depends on the server supporting the PCjs User API (see userapi.js), + * and since pcjs.org is no longer running a Node web server, we disable the feature for that + * particular host. + */ + if (Str.endsWith(Web.getHost(), "pcjs.org")) { + if (DEBUG) this.log("Remote user API not available"); + /* + * We could also simply hide the control; eg: + * + * control.style.display = "none"; + * + * but removing the control altogether seems better. + */ + control.parentNode.removeChild(/** @type {Node} */ (control)); + return false; + } + this.bindings[sBinding] = control; + control.onclick = function onClickSave() { + var sUserID = computer.queryUserID(true); + if (sUserID) { + /* + * I modified the test to include a check for sStatePath so that I could save new states + * for machines with existing states; otherwise, I'd have no (easy) way of capturing and + * updating their state. Making the machine (even temporarily) resumable would have been + * one work-around, but it's not appropriate for some machines, as their state is simply + * too large (for localStorage anyway, which is the default storage solution). + */ + var fSave = !!(computer.resume && !computer.sResumePath || computer.sStatePath); + var sState = computer.powerOff(fSave); + if (fSave) { + computer.saveServerState(sUserID, sState); + } else { + computer.notice("Resume disabled, machine state not saved"); + } + } + /* + * This seemed like a handy alternative, but it turned out to be a no-go, at least for large states: + * + * var sState = computer.powerOff(true); + * if (sState) { + * sState = "data:text/json;charset=utf-8," + encodeURIComponent(sState); + * window.open(sState); + * } + * + * Perhaps if I embedded the data in a link on the current page instead; eg: + * + * $('Download').appendTo('#container'); + */ + }; + return true; + + default: + break; + } + return false; + } + + /** + * resetUserID() + */ + resetUserID() + { + Web.setLocalStorageItem(ComputerPDP10.STATE_USERID, ""); + this.sUserID = null; + } + + /** + * queryUserID(fPrompt) + * + * @param {boolean} [fPrompt] + * @returns {string|null|undefined} + */ + queryUserID(fPrompt) + { + var sUserID = this.sUserID; + if (!sUserID) { + sUserID = Web.getLocalStorageItem(ComputerPDP10.STATE_USERID); + if (sUserID !== undefined) { + if (!sUserID && fPrompt) { + /* + * NOTE: Warning the user here that "Save" operations are not currently supported by pcjs.org is + * merely a precaution, because ordinarily, setBinding() should have already determined if we are + * running from pcjs.org and disabled any "Save" button. + */ + sUserID = Component.promptUser("Saving machine states on the pcjs.org server is currently unsupported.\n\nIf you're running your own server, enter your user ID below."); + if (sUserID) { + sUserID = this.verifyUserID(sUserID); + if (!sUserID) this.notice("The user ID is invalid."); + } + } + } else if (fPrompt) { + this.notice("Browser local storage is not available"); + } + } + return sUserID; + } + + /** + * verifyUserID(sUserID) + * + * @this {ComputerPDP10} + * @param {string} sUserID + * @return {string} validated user ID, or null if error + */ + verifyUserID(sUserID) + { + this.sUserID = null; + var fMessages = DEBUG && this.messageEnabled(); + if (fMessages) this.printMessage("verifyUserID(" + sUserID + ")"); + var sRequest = Web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUserID; + var response = Web.getResource(sRequest); + var nErrorCode = response[0]; + var sResponse = response[1]; + if (!nErrorCode && sResponse) { + try { + response = eval("(" + sResponse + ")"); + if (response.code && response.code == UserAPI.CODE.OK) { + Web.setLocalStorageItem(ComputerPDP10.STATE_USERID, response.data); + if (fMessages) this.printMessage(ComputerPDP10.STATE_USERID + " updated: " + response.data); + this.sUserID = response.data; + } else { + if (fMessages) this.printMessage(response.code + ": " + response.data); + } + } catch (e) { + Component.error(e.message + " (" + sResponse + ")"); + } + } else { + if (fMessages) this.printMessage("invalid response (error " + nErrorCode + ")"); + } + return this.sUserID; + } + + /** + * getServerStatePath() + * + * @this {ComputerPDP10} + * @return {string|null} sStatePath (null if no localStorage or no USERID stored in localStorage) + */ + getServerStatePath() + { + var sStatePath = null; + if (this.sUserID) { + if (DEBUG && this.messageEnabled()) { + this.printMessage(ComputerPDP10.STATE_USERID + " for load: " + this.sUserID); + } + sStatePath = Web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.LOAD + '&' + UserAPI.QUERY.USER + '=' + this.sUserID + '&' + UserAPI.QUERY.STATE + '=' + State.key(this, PDP10.APPVERSION); + } else { + if (DEBUG && this.messageEnabled()) { + this.printMessage(ComputerPDP10.STATE_USERID + " unavailable"); + } + } + return sStatePath; + } + + /** + * saveServerState(sUserID, sState) + * + * @param {string} sUserID + * @param {string|null} sState + */ + saveServerState(sUserID, sState) + { + /* + * We must pass fSync == true, because (as I understand it) browsers will blow off any async + * requests when a page is being closed. Since our request is synchronous, storeServerState() + * should also return a result, but there's not much we can do with it, since browsers ALSO + * tend to blow off alerts() and the like when closing down. + */ + if (sState) { + if (DEBUG && this.messageEnabled()) { + this.printMessage("size of server state: " + sState.length + " bytes"); + } + var response = this.storeServerState(sUserID, sState, true); + if (response && response[UserAPI.RES.CODE] == UserAPI.CODE.OK) { + this.notice("Machine state saved to server"); + } else if (sState) { + var sError = (response && response[UserAPI.RES.DATA]) || UserAPI.FAIL.BADSTORE; + if (response[UserAPI.RES.CODE] == UserAPI.CODE.FAIL) { + sError = "Error: " + sError; + } else { + sError = "Error " + response[UserAPI.RES.CODE] + ": " + sError; + } + this.notice(sError); + this.resetUserID(); + } + } else { + if (DEBUG && this.messageEnabled()) { + this.printMessage("no state to store"); + } + } + } + + /** + * storeServerState(sUserID, sState, fSync) + * + * @this {ComputerPDP10} + * @param {string} sUserID + * @param {string} sState + * @param {boolean} [fSync] is true if we're powering down and should perform a synchronous request (default is async) + * @return {*} server response if fSync is true and a response was received; otherwise null + */ + storeServerState(sUserID, sState, fSync) + { + if (DEBUG && this.messageEnabled()) { + this.printMessage(ComputerPDP10.STATE_USERID + " for store: " + sUserID); + } + /* + * TODO: Determine whether or not any browsers cancel our request if we're called during a browser "shutdown" event, + * and whether or not it matters if we do an async request (currently, we're not, to try to ensure the request goes through). + */ + var dataPost = {}; + dataPost[UserAPI.QUERY.REQ] = UserAPI.REQ.STORE; + dataPost[UserAPI.QUERY.USER] = sUserID; + dataPost[UserAPI.QUERY.STATE] = State.key(this, PDP10.APPVERSION); + dataPost[UserAPI.QUERY.DATA] = sState; + var sRequest = Web.getHost() + UserAPI.ENDPOINT; + if (!fSync) { + Web.getResource(sRequest, dataPost, true); + } else { + var response = Web.getResource(sRequest, dataPost); + var sResponse = response[0]; + if (response[1]) { + if (sResponse) { + var i = sResponse.indexOf('\n'); + if (i > 0) sResponse = sResponse.substr(0, i); + if (!sResponse.indexOf("Error: ")) sResponse = sResponse.substr(7); + } + sResponse = '{"' + UserAPI.RES.CODE + '":' + response[1] + ',"' + UserAPI.RES.DATA + '":"' + sResponse + '"}'; + } + if (DEBUG && this.messageEnabled()) this.printMessage(sResponse); + return JSON.parse(sResponse); + } + return null; + } + + /** + * onPower() + * + * This handles UI requests to toggle the computer's power (eg, see the "power" button binding). + * + * @this {ComputerPDP10} + */ + onPower() + { + if (!this.nPowerChange) { + if (!this.flags.powered) { + this.wait(this.powerOn); + } else { + this.powerOff(false, true); + } + } + } + + /** + * onReset() + * + * This handles UI requests to reset the computer's state (eg, see the "reset" button binding). + * + * @this {ComputerPDP10} + */ + onReset() + { + /* + * I'm going to start with the presumption that it makes little sense for an "unpowered" computer to be "reset"; + * ditto if the power state is currently being changed. + */ + if (!this.flags.powered || this.nPowerChange) return; + + /* + * If this is a "resumable" machine (and it's not using a predefined state), then we overload the reset + * operation to offer an explicit "save or discard" option first. This is currently the only UI we offer to + * discard a machine's state, including any disk changes. The traditional "reset" operation is still available + * for non-resumable machines. + * + * TODO: Break this behavior out into a separate "discard" operation, in case the designer of the machine really + * wants to clutter the UI with confusing options. ;-) + */ + if (this.resume && !this.sResumePath) { + /* + * I used to bypass the prompt if this.resume == ComputerPDP10.RESUME_AUTO, setting fSave to true automatically, + * but that gives the user no means of resetting a resumable machine that contains errors in its resume state. + */ + var fSave = (/* this.resume == ComputerPDP10.RESUME_AUTO || */ Component.confirmUser("Click OK to save changes to this " + PDP10.APPNAME + " machine.\n\nWARNING: If you CANCEL, all disk changes will be discarded.")); + this.powerOff(fSave, true); + /* + * Forcing the page to reload is an expedient option, but ugly. It's preferable to call powerOn() + * and rely on all the components to reset themselves to their default state. The components with + * the greatest burden here are FDC and HDC, which must rely on the fReload flag to determine whether + * or not to unload/reload all their original auto-mounted disk images. + * + * However, if we started with a predefined state (ie, sStatePath is set), we take this shortcut, because + * we don't (yet) have code in place to gracefully reload the initial state (requires calling getResource() + * again); alternatively, we could avoid throwing that state away, but it seems better to save the memory. + * + * TODO: Make this more graceful, so that we can stop using the reloadPage() sledgehammer. + */ + if (!fSave && this.sStatePath) { + Web.reloadPage(); + return; + } + if (!fSave) this.fReload = true; + this.powerOn(ComputerPDP10.RESUME_NONE); + this.fReload = false; + } else { + this.reset(); + if (this.cpu && !this.dbg) this.cpu.autoStart(); + } + } + + /** + * getMachineComponent(sType, componentPrev) + * + * @this {ComputerPDP10} + * @param {string} sType + * @param {Component|null} [componentPrev] of previously returned component, if any + * @return {Component|null} + */ + getMachineComponent(sType, componentPrev) + { + var componentLast = componentPrev; + var aComponents = Component.getComponents(this.id); + for (var iComponent = 0; iComponent < aComponents.length; iComponent++) { + var component = aComponents[iComponent]; + if (componentPrev) { + if (componentPrev == component) componentPrev = null; + continue; + } + if (component.type == sType) return component; + } + if (!componentLast) Component.log("Machine component type '" + sType + "' not found", "warning"); + return null; + } + + /** + * setFocus(fScroll) + * + * NOTE: When soft keyboard buttons call us to return focus to the machine (and away from the button), + * the browser's default behavior is to scroll the element into view, which can be annoying, especially on iOS, + * where the display is more constrained, so we no longer do it by default (fScroll must be true). + * + * @this {ComputerPDP10} + * @param {boolean} [fScroll] (true if you really want the control scrolled into view) + */ + setFocus(fScroll) + { + if (this.controlPrint) { + /* + * This seems to be recommended work-around to prevent the browser from scrolling the focused element + * into view. The CPU is not a visual component, so when the CPU wants to set focus, the primary intent + * is to ensure that keyboard input is fielded properly. + */ + var x = 0, y = 0; + if (!fScroll && window) { + x = window.scrollX; + y = window.scrollY; + } + + this.controlPrint.focus(); + + if (!fScroll && window) { + window.scrollTo(x, y); + } + } + } + + /** + * ComputerPDP10.init() + * + * For every machine represented by an HTML element of class "PDP10-machine", this function + * locates the HTML element of class "computer", extracting the JSON-encoded parameters for the + * Computer constructor from the element's "data-value" attribute, invoking the constructor to + * create a Computer component, and then binding any associated HTML controls to the new component. + */ + static init() + { + /* + * In non-COMPILED builds, embedMachine() may have set XMLVERSION. + */ + if (!COMPILED && XMLVERSION) PDP10.APPVERSION = XMLVERSION; + + var aeMachines = Component.getElementsByClass(document, PDP10.APPCLASS + "-machine"); + + for (var iMachine = 0; iMachine < aeMachines.length; iMachine++) { + + var eMachine = aeMachines[iMachine]; + var parmsMachine = Component.getComponentParms(eMachine); + + var aeComputers = Component.getElementsByClass(eMachine, PDP10.APPCLASS, "computer"); + + for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { + + var eComputer = aeComputers[iComputer]; + var parmsComputer = Component.getComponentParms(eComputer); + + /* + * We set fSuspended in the Computer constructor because we want to "power up" the + * computer ourselves, after any/all bindings are in place. + */ + var computer = new ComputerPDP10(parmsComputer, parmsMachine, true); + + if (DEBUG && computer.messageEnabled()) { + computer.printMessage("onInit(" + computer.flags.powered + ")"); + } + + /* + * Bind any "power", "reset" and "save" buttons. An "erase" button was also considered, + * but "reset" now provides a way to force the machine to start from scratch again, so "erase" + * may be redundant now. + */ + Component.bindComponentControls(computer, eComputer, PDP10.APPCLASS); + + /* + * Power on the computer, giving every component the opportunity to reset or restore itself. + */ + if (computer.fAutoPower) computer.wait(computer.powerOn); + } + } + } + + /** + * ComputerPDP10.show() + * + * When exit() is using an "onbeforeunload" handler, this "onpageshow" handler allows us to repower everything, + * without either resetting or restoring. We call powerOn() with a special resume value (RESUME_REPOWER) if the + * computer is already marked as "ready", meaning the browser didn't change anything. This "repower" process + * should be very quick, essentially just marking all components as powered again (so that, for example, the Video + * component will start drawing again) and firing the CPU up again. + */ + static show() + { + var aeComputers = Component.getElementsByClass(document, PDP10.APPCLASS, "computer"); + for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { + var eComputer = aeComputers[iComputer]; + var parmsComputer = Component.getComponentParms(eComputer); + var computer = /** @type {ComputerPDP10} */ (Component.getComponentByType("Computer", parmsComputer['id'])); + if (computer) { + + if (DEBUG && computer.messageEnabled()) { + computer.printMessage("onShow(" + computer.fInitialized + "," + computer.flags.powered + ")"); + } + + /* + * Note that the FIRST 'onpageshow' event, and therefore the first show() callback, occurs + * AFTER the the initial 'onload' event, and at that point in time, fInitialized will not be set yet. + * So, practically speaking, the first show() callback isn't all that useful. + */ + if (computer.fInitialized && !computer.flags.powered) { + /** + * Repower the computer, notifying every component to continue running as-is. + */ + computer.powerOn(ComputerPDP10.RESUME_REPOWER); + } + } + } + } + + /** + * ComputerPDP10.exit() + * + * The Computer is currently the only component that uses an "exit" handler, which Web.onExit() defines as + * either an "unload" or "onbeforeunload" handler. This gives us the opportunity to save the machine state, + * using our powerOff() function, before the page goes away. + * + * It's worth noting that "onbeforeunload" offers one nice feature when used instead of "onload": the entire + * page (and therefore this entire application) is retained in its current state by the browser (well, some + * browsers), so that if you go to a new URL, either by entering a new URL in the same window/tab, or by pressing + * the FORWARD button, and then you press the BACK button, the page is immediately restored to its previous state. + * + * In fact, that's how some browsers operate whether you have an "onbeforeunload" handler or not; in other words, + * an "onbeforeunload" handler doesn't change the page retention behavior of the browser. By contrast, the mere + * presence of an "onunload" handler generally causes a browser to throw the page away once the handler returns. + * + * However, in order to safely use "onbeforeunload", we must add yet another handler ("onpageshow") to repower + * everything, without either resetting or restoring. Hence, the ComputerPDP10.show() function, which calls powerOn() + * with a special resume value (RESUME_REPOWER) if the computer is already marked as "ready", meaning the browser + * didn't change anything. This "repower" process should be very quick, essentially just marking all components as + * powered again (so that, for example, the Video component will start drawing again) and firing the CPU up again. + * + * Reportedly, some browsers (eg, Opera) don't support "onbeforeunload", in which case Component will have to use + * "unload" instead. But even when the page must be rebuilt from scratch, the combination of browser cache and + * localStorage means the simulation should be restored and become operational almost immediately. + */ + static exit() + { + var aeComputers = Component.getElementsByClass(document, PDP10.APPCLASS, "computer"); + for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) { + var eComputer = aeComputers[iComputer]; + var parmsComputer = Component.getComponentParms(eComputer); + var computer = /** @type {ComputerPDP10} */ (Component.getComponentByType("Computer", parmsComputer['id'])); + if (computer) { + + if (DEBUG && computer.messageEnabled()) { + computer.printMessage("onExit(" + computer.flags.powered + ")"); + } + + if (computer.flags.powered) { + /** + * Power off the computer, giving every component an opportunity to save its state, + * but only if 'resume' has been set AND there is no valid resume path (because if a valid resume + * path exists, we'll always load our state from there, and not from whatever we save here). + */ + computer.powerOff(!!(computer.resume && !computer.sResumePath), true); + } + } + } + } +} + +ComputerPDP10.STATE_FAILSAFE = "failsafe"; +ComputerPDP10.STATE_VALIDATE = "validate"; +ComputerPDP10.STATE_TIMESTAMP = "timestamp"; +ComputerPDP10.STATE_VERSION = "version"; +ComputerPDP10.STATE_HOSTURL = "url"; +ComputerPDP10.STATE_BROWSER = "browser"; +ComputerPDP10.STATE_USERID = "user"; + +/* + * The following constants define all the resume options. Negative values (eg, RESUME_REPOWER) are for + * internal use only, and RESUME_DELETE is not documented (it provides a way of deleting ALL saved states + * whenever a resume is declined). As a result, the only "end-user" values are 0, 1 and 2. + */ +ComputerPDP10.RESUME_REPOWER = -1; // resume without changing any state (for internal use only) +ComputerPDP10.RESUME_NONE = 0; // default (no resume) +ComputerPDP10.RESUME_AUTO = 1; // automatically save/restore state +ComputerPDP10.RESUME_PROMPT = 2; // automatically save but conditionally restore (WARNING: if restore is declined, any state is discarded) +ComputerPDP10.RESUME_DELETE = 3; // same as RESUME_PROMPT but discards ALL machines states whenever ANY machine restore is declined (undocumented) + +/* + * Initialize every Computer on the page. + */ +Web.onInit(ComputerPDP10.init); +Web.onShow(ComputerPDP10.show); +Web.onExit(ComputerPDP10.exit); + +if (NODE) module.exports = ComputerPDP10; diff --git a/modules/pdp10/lib/cpu.js b/modules/pdp10/lib/cpu.js new file mode 100644 index 000000000..407662401 --- /dev/null +++ b/modules/pdp10/lib/cpu.js @@ -0,0 +1,1274 @@ +/** + * @fileoverview Controls the PDP-10 CPU component. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +if (NODE) { + var Str = require("../../shared/lib/strlib"); + var Component = require("../../shared/lib/component"); + var MessagesPDP10 = require("./messages"); +} + +/* + * A word (or more) about PDP-11 speeds: + * + * After looking over the timings of PDP-11/70 instructions, nearly all of them appear + * to be multiples of 150ns. So that's what we'll consider a cycle. How many 150ns are + * in one second? Approximately 6666667. So by way of comparison to other PCjs machines, + * that makes the PDP-11 (or at least the PDP-11/70) look like a 6.67Mhz machine. + * + * I've started with the PDP-11/70, since that's what Paul Nankervis started with. When + * I go back and add support for earlier PDP-11 models (primarily by neutering functions + * that didn't exist), I will no doubt have to tweak some instruction cycle counts, too. + * + * Examples of operations that take 1 extra cycle (150ns): single and double operand byte + * instructions with an odd address (except MOV/MTPI/MTPD/JMP/JRS), ADD/SUB/BIC/BIS/MOVB/CMP/BIT + * instructions with src of R1-R7 and dst of R6-R7, RORB/ASRB with an odd address, and each + * shift of ASH/ASHC. As you can see, the rules are not simple. + * + * We're not simulating cache hardware, but our timings should be optimistic and assume 100% + * cache hits; for cache hits, each read cycle is 300ns. As for write cycles, they are always + * 750ns. My initial take on DEC's timings is that they are including the write time as part + * of the total EF (execute/fetch) time. So, for instructions that write to memory, it looks + * like we'll normally need to add 5 cycles (750/150) to the instruction's base time, but + * we'll need to keep an eye out for exceptions. + */ + +/** + * @class CPUPDP10 + * @unrestricted + */ +class CPUPDP10 extends Component { + /** + * CPUPDP10(parmsCPU, nCyclesDefault) + * + * The CPUPDP10 class supports the following (parmsCPU) properties: + * + * cycles: the machine's base cycles per second; the CPUStatePDP10 constructor + * will provide us with a default (based on the CPU model) to use as a fallback. + * + * multiplier: base cycle multiplier; default is 1. + * + * autoStart: true to automatically start, false to not, or null if "it depends"; + * null is the default, which means do not autostart UNLESS there is no Debugger + * and no "Run" button (ie, no way to manually start the machine). + * + * csStart: the number of cycles that runCPU() must wait before generating + * checksum records; -1 if disabled. checksum records are a diagnostic aid + * used to help compare one CPU run to another. + * + * csInterval: the number of cycles that runCPU() must execute before generating + * a checksum record; -1 if disabled. + * + * csStop: the number of cycles to stop generating checksum records. + * + * This component is primarily responsible for interfacing the CPU with the outside + * world (eg, Panel and Debugger components), and managing overall CPU operation. + * + * It is extended by the CPUStatePDP10 component, where the simulation control logic resides. + * + * @param {Object} parmsCPU + * @param {number} nCyclesDefault + */ + constructor(parmsCPU, nCyclesDefault) + { + super("CPU", parmsCPU, MessagesPDP10.CPU); + + var nCycles = +parmsCPU['cycles'] || nCyclesDefault; + + var nMultiplier = +parmsCPU['multiplier'] || 1; + + this.nDisplayCount = 0; + this.nDisplayLimit = 30; + this.nCyclesPerSecond = nCycles; + + /* + * nCyclesMultiplier replaces the old "speed" variable (0, 1, 2) and eliminates the need for + * the constants (SPEED_SLOW, SPEED_FAST and SPEED_MAX). The UI simply doubles the multiplier + * until we've exceeded the host's speed limit and then starts the multiplier over at 1. + */ + this.nCyclesMultiplier = nMultiplier; + this.mhzDefault = Math.round(this.nCyclesPerSecond / 10000) / 100; + this.mhzTarget = this.mhzDefault * this.nCyclesMultiplier; + this.msPerYield = this.nCyclesPerYield = this.nCyclesNextYield = this.nCyclesRecalc = 0; + + /* + * We add a number of flags to the set initialized by Component + */ + this.flags.running = this.flags.starting = false; + this.flags.autoStart = parmsCPU['autoStart']; + if (typeof this.flags.autoStart == "string") this.flags.autoStart = (this.flags.autoStart == "true"); + + /* + * Get checksum parameters, if any. runCPU() behavior is not affected until fChecksum + * is true, which won't happen until resetChecksum() is called with nCyclesChecksumInterval + * ("csInterval") set to a positive value. + * + * As above, any of these parameters can also be set with the Debugger's execution options + * command ("x"); for example, "x cs int 5000" will set nCyclesChecksumInterval to 5000 + * and call resetChecksum(). + */ + this.flags.checksum = false; + this.nChecksum = this.nCyclesChecksumNext = 0; + this.nCyclesChecksumStart = +parmsCPU["csStart"]; + this.nCyclesChecksumInterval = +parmsCPU["csInterval"]; + this.nCyclesChecksumStop = +parmsCPU["csStop"]; + + /* + * Array of countdown timers managed by addTimer() and setTimer(). + */ + this.aTimers = []; + + this.onRunTimeout = this.runCPU.bind(this); // function onRunTimeout() { cpu.runCPU(); }; + + /* + * Define the rest of the properties used by the class + */ + this.mhz = 0; + this.nYieldsSinceStatusUpdate = 0; + this.msStartRun = this.msStartThisRun = this.msEndThisRun = this.nCyclesThisRun = 0; + this.nTotalCycles = this.nRunCycles = this.nBurstCycles = this.nStepCycles = this.nSnapCycles = 0; + this.panel = null; + + this.setReady(); + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * @this {CPUPDP10} + * @param {ComputerPDP10} cmp + * @param {BusPDP10} bus + * @param {CPUPDP10} cpu + * @param {DebuggerPDP10} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.cmp = cmp; + this.bus = bus; + this.dbg = dbg; + this.panel = cmp.panel; + for (var i = 0; i < CPUPDP10.BUTTONS.length; i++) { + var control = this.bindings[CPUPDP10.BUTTONS[i]]; + if (control) this.cmp.setBinding(null, CPUPDP10.BUTTONS[i], control); + } + this.setReady(); + } + + /** + * reset() + * + * Stub for reset notification (overridden by the CPUStatePDP10 component). + * + * @this {CPUPDP10} + */ + reset() + { + } + + /** + * save() + * + * Stub for save support (overridden by the CPUStatePDP10 component). + * + * @this {CPUPDP10} + * @return {Object|null} + */ + save() + { + return null; + } + + /** + * restore(data) + * + * Stub for restore support (overridden by the CPUStatePDP10 component). + * + * @this {CPUPDP10} + * @param {Object} data + * @return {boolean} true if restore successful, false if not + */ + restore(data) + { + return false; + } + + /** + * powerUp(data, fRepower) + * + * @this {CPUPDP10} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + /* + * We've already saved the parmsCPU 'autoStart' setting, but there may be a machine (or URL) override. + */ + var sAutoStart = this.cmp.getMachineParm('autoStart'); + if (sAutoStart != null) { + this.flags.autoStart = (sAutoStart == "true"? true : (sAutoStart == "false"? false : !!sAutoStart)); + } + else if (this.flags.autoStart == null) { + /* + * If there's no explicit parmsCPU setting either, then we will autoStart if there's no Debugger and + * no "Run" button. + */ + this.flags.autoStart = ((!DEBUGGER || !this.dbg) && this.bindings["run"] === undefined); + } + + if (!fRepower) { + if (!data) { + this.reset(); + } else { + this.resetCycles(); + if (!this.restore(data)) return false; + this.resetChecksum(); + } + /* + * Give the Debugger a chance to do/print something once we've powered up. + */ + if (DEBUGGER && this.dbg) { + this.dbg.init(this.flags.autoStart); + } else { + /* + * The Computer (this.cmp) knows if there's a Control Panel (this.panel), and the Control Panel + * knows if there's a "print" control (this.panel.controlPrint), and if there IS a "print" control + * but no debugger, the machine is probably misconfigured (most likely, the page simply neglected to + * load the Debugger component). + * + * However, we don't actually need to check all that; it's always safe use println(), regardless whether + * a Control Panel with a "print" control is present or not. + */ + this.status("No debugger detected"); + } + if (!this.flags.autoStart) { + this.println("CPU will not be auto-started " + (this.panel? "(click Run to start)" : "(type 'go' to start)")); + } + } + /* + * The Computer component (which is responsible for all powerDown and powerUp notifications) + * is now responsible for managing a component's fPowered flag, not us. + * + * this.flags.powered = true; + */ + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * @this {CPUPDP10} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + return fSave? this.save() : true; + } + + /** + * autoStart() + * + * @this {CPUPDP10} + * @return {boolean} true if started, false if not + */ + autoStart() + { + if (this.flags.running) { + return true; + } + if (this.flags.autoStart) { + /* + * We used to also set fUpdateFocus when calling startCPU(), on the assumption that in the "auto-starting" + * context, a machine without focus is like a day without sunshine, but in reality, focus should only be + * forced when the user takes some other machine-related action. + */ + this.startCPU(); + return true; + } + return false; + } + + /** + * isPowered() + * + * @this {CPUPDP10} + * @return {boolean} + */ + isPowered() + { + if (!this.flags.powered) { + this.println(this.toString() + " not powered"); + return false; + } + return true; + } + + /** + * isRunning() + * + * @this {CPUPDP10} + * @return {boolean} + */ + isRunning() + { + return this.flags.running; + } + + /** + * getChecksum() + * + * This will be implemented by the CPUStatePDP10 component. + * + * @this {CPUPDP10} + * @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code) + */ + getChecksum() + { + return 0; + } + + /** + * resetChecksum() + * + * If checksum generation is enabled (fChecksum is true), this resets the running 32-bit checksum and the + * cycle counter that will trigger the next displayChecksum(); called by resetCycles(), which is called whenever + * the CPU is reset or restored. + * + * @this {CPUPDP10} + * @return {boolean} true if checksum generation enabled, false if not + */ + resetChecksum() + { + if (this.nCyclesChecksumStart === undefined) this.nCyclesChecksumStart = 0; + if (this.nCyclesChecksumInterval === undefined) this.nCyclesChecksumInterval = -1; + if (this.nCyclesChecksumStop === undefined) this.nCyclesChecksumStop = -1; + this.flags.checksum = (this.nCyclesChecksumStart >= 0 && this.nCyclesChecksumInterval > 0); + if (this.flags.checksum) { + this.nChecksum = 0; + this.nCyclesChecksumNext = this.nCyclesChecksumStart - this.nTotalCycles; + /* + * this.nCyclesChecksumNext = this.nCyclesChecksumStart + this.nCyclesChecksumInterval - + * (this.nTotalCycles % this.nCyclesChecksumInterval); + */ + return true; + } + return false; + } + + /** + * updateChecksum(nCycles) + * + * When checksum generation is enabled (fChecksum is true), runCPU() asks stepCPU() to execute a minimum + * number of cycles (1), effectively limiting execution to a single instruction, and then we're called with + * the exact number cycles that were actually executed. This should give us instruction-granular checksums + * at precise intervals that are 100% repeatable. + * + * @this {CPUPDP10} + * @param {number} nCycles + */ + updateChecksum(nCycles) + { + if (this.flags.checksum) { + /* + * Get a 32-bit summation of the current CPU state and add it to our running 32-bit checksum + */ + var fDisplay = false; + this.nChecksum = (this.nChecksum + this.getChecksum())|0; + this.nCyclesChecksumNext -= nCycles; + if (this.nCyclesChecksumNext <= 0) { + this.nCyclesChecksumNext += this.nCyclesChecksumInterval; + fDisplay = true; + } + if (this.nCyclesChecksumStop >= 0) { + if (this.nCyclesChecksumStop <= this.getCycles()) { + this.nCyclesChecksumInterval = this.nCyclesChecksumStop = -1; + this.resetChecksum(); + this.stopCPU(); + fDisplay = true; + } + } + if (fDisplay) this.displayChecksum(); + } + } + + /** + * displayChecksum() + * + * When checksum generation is enabled (fChecksum is true), this is called to provide a crude log of all + * checksums generated at the specified cycle intervals, as specified by the "csStart" and "csInterval" parmsCPU + * properties). + * + * @this {CPUPDP10} + */ + displayChecksum() + { + this.println(this.getCycles() + " cycles: " + "checksum=" + Str.toHex(this.nChecksum)); + } + + /** + * setBinding(sType, sBinding, control, sValue) + * + * @this {CPUPDP10} + * @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc) + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "run") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sType, sBinding, control, sValue) + { + var cpu = this; + + switch (sBinding) { + case "power": + case "reset": + /* + * The "power" and "reset" buttons are functions of the entire computer, not just the CPU, + * but it's not always convenient to stick a power button in the Computer component definition, + * so we record those bindings here and pass them on to the Computer component in initBus(). + */ + this.bindings[sBinding] = control; + return true; + + case "run": + this.bindings[sBinding] = control; + control.onclick = function onClickRun() { + if (!cpu.cmp || !cpu.cmp.checkPower()) return; + /* + * We no longer pass true to these startCPU()/stopCPU() calls, on the theory that if the "run" + * control is visible, then the computer is probably sufficiently visible as well; the problem + * with setting fUpdateFocus to true is that it can jerk the web page around in annoying ways. + */ + if (!cpu.flags.running) + cpu.startCPU(); + else + cpu.stopCPU(); + }; + return true; + + case "speed": + this.bindings[sBinding] = control; + return true; + + case "setSpeed": + this.bindings[sBinding] = control; + control.onclick = function onClickSetSpeed() { + cpu.setSpeed(cpu.nCyclesMultiplier << 1, true); + }; + control.textContent = this.getSpeedTarget(); + return true; + + default: + break; + } + return false; + } + + /** + * updateDisplays(nUpdate) + * + * Simpler wrapper around the Computer's updateDisplays() method. + * + * @this {CPUPDP10} + * @param {number} [nUpdate] (1 for periodic, -1 for forced, 0 or undefined otherwise) + */ + updateDisplays(nUpdate) + { + if (this.cmp) this.cmp.updateDisplays(nUpdate); + } + + /** + * updateDisplay(nUpdate) + * + * Some of the CPU bindings provide feedback and therefore need to be updated periodically. + * However, this should be called via the Computer's updateDisplays() interface, not directly. + * + * @this {CPUPDP10} + * @param {number} [nUpdate] (1 for periodic, -1 for forced, 0 otherwise) + */ + updateDisplay(nUpdate) + { + var controlSpeed = this.bindings["speed"]; + if (controlSpeed) { + if (nUpdate <= 0 || (this.nDisplayCount += nUpdate) >= this.nDisplayLimit) { + controlSpeed.textContent = this.getSpeedCurrent(); + this.nDisplayCount = 0; + } + } + } + + /** + * addCycles(nCycles, fEndStep) + * + * @this {CPUPDP10} + * @param {number} nCycles + * @param {boolean} [fEndStep] + */ + addCycles(nCycles, fEndStep) + { + this.nTotalCycles += nCycles; + if (fEndStep) { + this.nBurstCycles = this.nStepCycles = this.nSnapCycles = 0; + } + } + + /** + * calcCycles(fRecalc) + * + * Calculate the number of cycles to process for each "burst" of CPU activity. The size of a burst + * is driven by YIELDS_PER_SECOND (eg, 30). + * + * At the end of each burst, we subtract burst cycles from the yield cycle "threshold" counter. + * Whenever the "next yield" cycle counter goes to (or below) zero, we compare elapsed time to the time + * we expected the virtual hardware to take (eg, 1000ms/50 or 20ms), and if we still have time remaining, + * we sleep the remaining time (or 0ms if there's no remaining time), and then restart runCPU(). + * + * @this {CPUPDP10} + * @param {boolean} [fRecalc] is true if the caller wants to recalculate thresholds based on the most recent + * speed calculation (see calcSpeed). + */ + calcCycles(fRecalc) + { + /* + * Calculate "per" yield values. + */ + var vMultiplier = 1; + if (fRecalc) { + if (this.nCyclesMultiplier > 1 && this.mhz) { + vMultiplier = (this.mhz / this.mhzDefault); + } + } + + this.msPerYield = Math.round(1000 / CPUPDP10.YIELDS_PER_SECOND); + this.nCyclesPerYield = Math.floor(this.nCyclesPerSecond / CPUPDP10.YIELDS_PER_SECOND * vMultiplier); + + /* + * And initialize "next" yield values to the "per" values. + */ + if (!fRecalc) this.nCyclesNextYield = this.nCyclesPerYield; + this.nCyclesRecalc = 0; + } + + /** + * getCycles(fScaled) + * + * getCycles() returns the number of cycles executed so far. Note that we can be called after + * runCPU() OR during runCPU(), perhaps from a handler triggered during the current run's stepCPU(), + * so nRunCycles must always be adjusted by number of cycles stepCPU() was asked to run (nBurstCycles), + * less the number of cycles it has yet to run (nStepCycles). + * + * nRunCycles is zeroed whenever the CPU is halted or the CPU speed is changed, which is why we also + * have nTotalCycles, which accumulates all nRunCycles before we zero it. However, nRunCycles and + * nTotalCycles eventually get reset by calcSpeed(), to avoid overflow, so components that rely on + * getCycles() returning steadily increasing values should also be prepared for a reset at any time. + * + * @this {CPUPDP10} + * @param {boolean} [fScaled] is true if the caller wants a cycle count relative to a multiplier of 1 + * @return {number} + */ + getCycles(fScaled) + { + var nCycles = this.nTotalCycles + this.nRunCycles + this.nBurstCycles - this.nStepCycles; + if (fScaled && this.nCyclesMultiplier > 1 && this.mhz > this.mhzDefault) { + /* + * We could scale the current cycle count by the current effective speed (this.mhz); eg: + * + * nCycles = Math.round(nCycles / (this.mhz / this.mhzDefault)); + * + * but that speed will fluctuate somewhat: large fluctuations at first, but increasingly smaller + * fluctuations after each burst of instructions that runCPU() executes. + * + * Alternatively, we can scale the cycle count by the multiplier, which is good in that the + * multiplier doesn't vary once the user changes it, but a potential downside is that the + * multiplier might be set too high, resulting in a target speed that's higher than the effective + * speed is able to reach. + * + * Also, if multipliers were always limited to a power-of-two, then this could be calculated + * with a simple shift. However, only the "setSpeed" UI binding limits it that way; the Debugger + * interface allows any value, as does the CPU "multiplier" parmsCPU property (from the machine's + * XML file). + */ + nCycles = Math.round(nCycles / this.nCyclesMultiplier); + } + return nCycles; + } + + /** + * getCyclesPerSecond() + * + * This returns the CPU's "base" speed (ie, the original cycles per second defined for the machine) + * + * @this {CPUPDP10} + * @return {number} + */ + getCyclesPerSecond() + { + return this.nCyclesPerSecond; + } + + /** + * resetCycles() + * + * Resets speed and cycle information as part of any reset() or restore(); this typically occurs during powerUp(). + * It's important that this be called BEFORE the actual restore() call, because restore() may want to call setSpeed(), + * which in turn assumes that all the cycle counts have been initialized to sensible values. + * + * @this {CPUPDP10} + */ + resetCycles() + { + this.mhz = 0; + this.nYieldsSinceStatusUpdate = 0; + this.nTotalCycles = this.nRunCycles = this.nBurstCycles = this.nStepCycles = this.nSnapCycles = 0; + this.resetChecksum(); + this.setSpeed(1); + } + + /** + * getSpeed() + * + * @this {CPUPDP10} + * @return {number} the current speed multiplier + */ + getSpeed() + { + return this.nCyclesMultiplier; + } + + /** + * getSpeedCurrent() + * + * @this {CPUPDP10} + * @return {string} the current speed, in mhz, as a string formatted to two decimal places + */ + getSpeedCurrent() + { + /* + * TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now? + */ + return ((this.flags.running)? (this.mhz.toFixed(2) + "Mhz") : "Stopped"); + } + + /** + * getSpeedTarget() + * + * @this {CPUPDP10} + * @return {string} the target speed, in mhz, as a string formatted to two decimal places + */ + getSpeedTarget() + { + /* + * TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now? + */ + return this.mhzTarget.toFixed(2) + "Mhz"; + } + + /** + * setSpeed(nMultiplier, fUpdateFocus) + * + * NOTE: This used to return the target speed, in mhz, but no callers appear to care at this point. + * + * @desc Whenever the speed is changed, the running cycle count and corresponding start time must be reset, + * so that the next effective speed calculation obtains sensible results. In fact, when runCPU() initially calls + * setSpeed() with no parameters, that's all this function does (it doesn't change the current speed setting). + * + * @this {CPUPDP10} + * @param {number} [nMultiplier] is the new proposed multiplier (reverts to 1 if the target was too high) + * @param {boolean} [fUpdateFocus] is true to update Computer focus + * @return {boolean} true if successful, false if not + */ + setSpeed(nMultiplier, fUpdateFocus) + { + var fSuccess = false; + if (nMultiplier !== undefined) { + /* + * If we haven't reached 80% (0.8) of the current target speed, revert to a multiplier of one (1). + */ + if (this.mhz / this.mhzTarget < 0.8) { + nMultiplier = 1; + } else { + fSuccess = true; + } + this.nCyclesMultiplier = nMultiplier; + var mhz = this.mhzDefault * this.nCyclesMultiplier; + if (this.mhzTarget != mhz) { + this.mhzTarget = mhz; + var sSpeed = this.getSpeedTarget(); + var controlSpeed = this.bindings["setSpeed"]; + if (controlSpeed) controlSpeed.textContent = sSpeed; + this.println("target speed: " + sSpeed); + } + if (fUpdateFocus && this.cmp) this.cmp.setFocus(); + } + this.addCycles(this.nRunCycles); + this.nRunCycles = 0; + this.msStartRun = Component.getTime(); + this.msEndThisRun = 0; + this.calcCycles(); + return fSuccess; + } + + /** + * calcSpeed(nCycles, msElapsed) + * + * @this {CPUPDP10} + * @param {number} nCycles + * @param {number} msElapsed + */ + calcSpeed(nCycles, msElapsed) + { + if (msElapsed) { + this.mhz = Math.round(nCycles / (msElapsed * 10)) / 100; + if (msElapsed >= 86400000) { + this.nTotalCycles = 0; + this.setSpeed(); // reset all counters once per day so that we never have to worry about overflow + } + } + } + + /** + * calcStartTime() + * + * @this {CPUPDP10} + */ + calcStartTime() + { + if (this.nCyclesRecalc >= this.nCyclesPerSecond) { + this.calcCycles(true); + } + this.nCyclesThisRun = 0; + this.msStartThisRun = Component.getTime(); + + /* + * Try to detect situations where the browser may have throttled us, such as when the user switches + * to a different tab; in those situations, Chrome and Safari may restrict setTimeout() callbacks + * to roughly one per second. + * + * Another scenario: the user resizes the browser window. setTimeout() callbacks are not throttled, + * but there can still be enough of a lag between the callbacks that CPU speed will be noticeably + * erratic if we don't compensate for it here. + * + * We can detect throttling/lagging by verifying that msEndThisRun (which was set at the end of the + * previous run and includes any requested sleep time) is comparable to the current msStartThisRun; + * if the delta is significant, we compensate by bumping msStartRun forward by that delta. + * + * This shouldn't be triggered when the Debugger halts the CPU, because setSpeed() -- which is called + * whenever the CPU starts running again -- zeroes msEndThisRun. + * + * This also won't do anything about other internal delays; for example, Debugger message() calls. + * By the time the message() function has called yieldCPU(), the cost of the message has already been + * incurred, so it will be end up being charged against the instruction(s) that triggered it. + * + * TODO: Consider calling yieldCPU() sooner from message(), so that it can arrange for the msEndThisRun + * "snapshot" to occur sooner; it's unclear, however, whether that will really improve the CPU's ability + * to hit its target speed, since you would expect any instruction that displays a message to be an + * EXTREMELY slow instruction. + */ + if (this.msEndThisRun) { + var msDelta = this.msStartThisRun - this.msEndThisRun; + if (msDelta > this.msPerYield) { + if (MAXDEBUG) this.println("large time delay: " + msDelta + "ms"); + this.msStartRun += msDelta; + /* + * Bumping msStartRun forward should NEVER cause it to exceed msStartThisRun; however, just + * in case, I make absolutely sure it cannot happen, since doing so could result in negative + * speed calculations. + */ + this.assert(this.msStartRun <= this.msStartThisRun); + if (this.msStartRun > this.msStartThisRun) { + this.msStartRun = this.msStartThisRun; + } + } + } + } + + /** + * calcRemainingTime() + * + * @this {CPUPDP10} + * @return {number} + */ + calcRemainingTime() + { + this.msEndThisRun = Component.getTime(); + + var msYield = this.msPerYield; + if (this.nCyclesThisRun) { + /* + * Normally, we would assume we executed a full quota of work over msPerYield, but since the CPU + * now has the option of calling yieldCPU(), that might not be true. If nCyclesThisRun is correct, then + * the ratio of nCyclesThisRun/nCyclesPerYield should represent the percentage of work we performed, + * and so applying that percentage to msPerYield should give us a better estimate of work vs. time. + */ + msYield = Math.round(msYield * this.nCyclesThisRun / this.nCyclesPerYield); + } + + var msElapsedThisRun = this.msEndThisRun - this.msStartThisRun; + var msRemainsThisRun = msYield - msElapsedThisRun; + + /* + * We could pass only "this run" results to calcSpeed(): + * + * nCycles = this.nCyclesThisRun; + * msElapsed = msElapsedThisRun; + * + * but it seems preferable to use longer time periods and hopefully get a more accurate speed. + * + * Also, if msRemainsThisRun >= 0 && this.nCyclesMultiplier == 1, we could pass these results instead: + * + * nCycles = this.nCyclesThisRun; + * msElapsed = this.msPerYield; + * + * to insure that we display a smooth, constant N Mhz. But for now, I prefer seeing any fluctuations. + */ + var nCycles = this.nRunCycles; + var msElapsed = this.msEndThisRun - this.msStartRun; + + if (MAXDEBUG && msRemainsThisRun < 0 && this.nCyclesMultiplier > 1) { + this.println("warning: updates @" + msElapsedThisRun + "ms (prefer " + Math.round(msYield) + "ms)"); + } + + this.calcSpeed(nCycles, msElapsed); + + if (msRemainsThisRun < 0 || this.mhz < this.mhzTarget) { + /* + * Try "throwing out" the effects of large anomalies, by moving the overall run start time up; + * ordinarily, this should only happen when the someone is using an external Debugger or some other + * tool or feature that is interfering with our overall execution. + */ + if (msRemainsThisRun < -1000) { + this.msStartRun -= msRemainsThisRun; + } + /* + * If the last burst took MORE time than we allotted (ie, it's taking more than 1 second to simulate + * nCyclesPerSecond), all we can do is yield for as little time as possible (ie, 0ms) and hope that the + * simulation is at least usable. + */ + msRemainsThisRun = 0; + } + + /* + * Last but not least, update nCyclesRecalc, so that when runCPU() starts up again and calls calcStartTime(), + * it'll be ready to decide if calcCycles() should be called again. + */ + this.nCyclesRecalc += this.nCyclesThisRun; + + if (DEBUG && this.messageEnabled(MessagesPDP10.LOG) && msRemainsThisRun) { + this.log("calcRemainingTime: " + msRemainsThisRun + "ms to sleep after " + this.msEndThisRun + "ms"); + } + + this.msEndThisRun += msRemainsThisRun; + return msRemainsThisRun; + } + + /** + * addTimer(callBack) + * + * Components that want to have timers that periodically fire after some number of milliseconds call + * addTimer() to create the timer, and then setTimer() every time they want to arm it. There is currently + * no removeTimer() because these are generally used for the entire lifetime of a component. + * + * Internally, each timer entry is a preallocated Array with two entries: a cycle countdown in element [0] + * and a callback function in element [1]. A timer is initially dormant; dormant timers have a countdown + * value of -1 (although any negative number will suffice) and active timers have a non-negative value. + * + * Why not use JavaScript's setTimeout() instead? Good question. For a good answer, see setTimer() below. + * + * TODO: Consider making the addTimer() and setTimer() interfaces more like the addIRQ() and setIRQ() + * interfaces (which return the underlying object instead of an array index) and maintaining a separate list + * of active timers, in order of highest to lowest cycle countdown values, as this could speed up + * getBurstCycles() and updateTimers() functions ever so slightly. + * + * @this {CPUPDP10} + * @param {function()} callBack + * @return {number} timer index + */ + addTimer(callBack) + { + var iTimer = this.aTimers.length; + this.aTimers.push([-1, callBack]); + return iTimer; + } + + /** + * setTimer(iTimer, ms, fReset) + * + * Using the timer index from a previous addTimer() call, this sets that timer to fire after the + * specified number of milliseconds. + * + * This is preferred over JavaScript's setTimeout(), because all our timers are effectively paused when + * the CPU is paused (eg, when the Debugger halts execution). Moreover, setTimeout() handlers only run after + * runCPU() yields, which is far too granular for some components (eg, when the SerialPort tries to simulate + * interrupts at 9600 baud). + * + * Ideally, the only function that would use setTimeout() is runCPU(), while the rest of the components + * use setTimer(); however, due to legacy code (ie, code that predates these functions) and/or laziness, + * that may not be the case. + * + * @this {CPUPDP10} + * @param {number} iTimer + * @param {number} ms (converted into a cycle countdown internally) + * @param {boolean} [fReset] (true if the timer should be reset even if already armed) + * @return {number} (number of cycles used to arm timer, or -1 if error) + */ + setTimer(iTimer, ms, fReset) + { + var nCycles = -1; + if (iTimer >= 0 && iTimer < this.aTimers.length) { + if (fReset || this.aTimers[iTimer][0] < 0) { + nCycles = this.getMSCycles(ms); + /* + * We must now confront the following problem: if the CPU is currently executing a burst of cycles, + * the number of cycles it has executed in that burst so far must NOT be charged against the cycle + * timeout we're about to set. The simplest way to resolve that is to immediately call endBurst() + * and bias the cycle timeout by the number of cycles that the burst executed. + */ + if (this.flags.running) { + nCycles += this.endBurst(); + } + this.aTimers[iTimer][0] = nCycles; + } + } + return nCycles; + } + + /** + * getMSCycles(ms) + * + * @this {CPUPDP10} + * @param {number} ms + * @return {number} number of corresponding cycles + */ + getMSCycles(ms) + { + return ((this.nCyclesPerSecond * this.nCyclesMultiplier) / 1000 * ms)|0; + } + + /** + * getBurstCycles(nCycles) + * + * Used by runCPU() to get min(nCycles,[timer cycle counts]) + * + * @this {CPUPDP10} + * @param {number} nCycles (number of cycles about to execute) + * @return {number} (either nCycles or less if a timer needs to fire) + */ + getBurstCycles(nCycles) + { + for (var i = this.aTimers.length - 1; i >= 0; i--) { + var timer = this.aTimers[i]; + this.assert(!isNaN(timer[0])); + if (timer[0] < 0) continue; + if (nCycles > timer[0]) { + nCycles = timer[0]; + } + } + return nCycles; + } + + /** + * saveTimers() + * + * @this {CPUPDP10} + * @return {Array.} + */ + saveTimers() + { + var aTimerCycles = []; + for (var i = 0; i < this.aTimers.length; i++) { + var timer = this.aTimers[i]; + aTimerCycles.push(timer[0]); + } + return aTimerCycles; + } + + /** + * restoreTimers(aTimerCycles) + * + * @this {CPUPDP10} + * @param {Array.} aTimerCycles + */ + restoreTimers(aTimerCycles) + { + this.assert(aTimerCycles.length === this.aTimers.length); + for (var i = 0; i < this.aTimers.length && i < aTimerCycles.length; i++) { + var timer = this.aTimers[i]; + timer[0] = aTimerCycles[i]; + } + } + + /** + * updateTimers(nCycles) + * + * Used by runCPU() to reduce all active timer countdown values by the number of cycles just executed; + * this is the function that actually "fires" any timer(s) whose countdown has reached (or dropped below) + * zero, invoking their callback function. + * + * @this {CPUPDP10} + * @param {number} nCycles (number of cycles actually executed) + */ + updateTimers(nCycles) + { + for (var i = this.aTimers.length - 1; i >= 0; i--) { + var timer = this.aTimers[i]; + this.assert(!isNaN(timer[0])); + if (timer[0] < 0) continue; + timer[0] -= nCycles; + if (timer[0] <= 0) { + timer[0] = -1; // zero is technically an "active" value, so ensure the timer is dormant now + timer[1](); // safe to invoke the callback function now + } + } + } + + /** + * endBurst(fReset) + * + * @this {CPUPDP10} + * @param {boolean} [fReset] + * @return {number} (number of cycles executed in the most recent burst) + */ + endBurst(fReset) + { + var nCycles = this.nBurstCycles -= this.nStepCycles; + /* + * In addition to zeroing nStepCycles, it's important that we also zero nSnapCycles, because if a CPU + * burst is being ended after nStepCycles has been "snapped" (because a certain opcode has an unusual timing + * calculation that must be based on a "snapped" cycle count rather the opcode's starting cycle count), we + * could inadvertently undo the endBurst() if the original "snapped" value was used to update nStepCycles. + */ + this.nStepCycles = this.nSnapCycles = 0; + if (fReset) this.nBurstCycles = 0; + return nCycles; + } + + /** + * runCPU() + * + * @this {CPUPDP10} + */ + runCPU() + { + if (!this.flags.running) return; + + /* + * calcStartTime() initializes the cycle counter and timestamp for this runCPU() invocation, and optionally + * recalculates the the maximum number of cycles for each burst if the nCyclesRecalc threshold has been reached. + */ + this.calcStartTime(); + + try { + do { + /* + * nCycles is how many cycles we WANT to run on each iteration of stepCPU(), and may be as + * HIGH as nCyclesPerYield, but it may be significantly less. getBurstCycles() will adjust + * nCycles downward if any CPU timers need to fire during the next burst. + */ + var nCycles = this.getBurstCycles(this.flags.checksum? 1 : this.nCyclesPerYield); + + /* + * Execute the burst. + */ + try { + this.stepCPU(nCycles); + } + catch(exception) { + /* + * We assume that any numeric exception was explicitly thrown by the CPU to interrupt the + * current instruction (and by extension, the current burst, but not the current run). All + * other exceptions are re-thrown to the catch below, which will attempt a stack dump. + */ + if (typeof exception != "number") throw exception; + } + + /* + * Terminate the burst, returning the number of cycles that stepCPU() actually ran. + */ + nCycles = this.endBurst(true); + + /* + * Add nCycles to nCyclesThisRun, as well as nRunCycles (the cycle count since the CPU started). + */ + this.nCyclesThisRun += nCycles; + this.nRunCycles += nCycles; + this.updateChecksum(nCycles); + + /* + * Update any/all timers, firing those whose cycle countdowns have reached (or dropped below) zero. + */ + this.updateTimers(nCycles); + + this.nCyclesNextYield -= nCycles; + if (this.nCyclesNextYield <= 0) { + this.nCyclesNextYield += this.nCyclesPerYield; + if (++this.nYieldsSinceStatusUpdate >= CPUPDP10.YIELDS_PER_STATUS) { + this.updateDisplays(); + this.nYieldsSinceStatusUpdate = 0; + } + break; + } + } while (this.flags.running); + } + catch (e) { + this.stopCPU(); + if (this.cmp) this.cmp.stop(Component.getTime(), this.getCycles()); + this.setError(e.stack || e.message); + return; + } + + if (this.flags.running) setTimeout(this.onRunTimeout, this.calcRemainingTime()); + } + + /** + * startCPU(fUpdateFocus) + * + * For use by any component that wants to start the CPU. + * + * @param {boolean} [fUpdateFocus] + * @return {boolean} + */ + startCPU(fUpdateFocus) + { + if (this.isError()) { + return false; + } + if (this.flags.running) { + this.println(this.toString() + " busy"); + return false; + } + /* + * setSpeed() without a speed parameter leaves the selected speed in place, but also resets the + * cycle counter and timestamp for the current series of runCPU() calls, calculates the maximum number + * of cycles for each burst based on the last known effective CPU speed, and resets the nCyclesRecalc + * threshold counter. + */ + this.setSpeed(); + this.flags.running = true; + this.flags.starting = true; + var controlRun = this.bindings["run"]; + if (controlRun) controlRun.textContent = "Halt"; + if (this.cmp) { + if (fUpdateFocus) this.cmp.setFocus(true); + this.cmp.start(this.msStartRun, this.getCycles()); + } + if (!this.dbg) this.status("Started"); + setTimeout(this.onRunTimeout, 0); + return true; + } + + /** + * stepCPU(nMinCycles) + * + * This will be implemented by the CPUStatePDP10 component. + * + * @this {CPUPDP10} + * @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored) + * @return {number} of cycles executed; 0 indicates that the last instruction was not executed + */ + stepCPU(nMinCycles) + { + return 0; + } + + /** + * stopCPU(fComplete) + * + * For use by any component that wants to stop the CPU. + * + * This similar to yieldCPU(), but it doesn't need to zero nCyclesNextYield to break out of runCPU(); + * it simply needs to clear fRunning (well, "simply" may be oversimplifying a bit....) + * + * @this {CPUPDP10} + * @param {boolean} [fComplete] + * @return {boolean} true if the CPU was stopped, false if it was already stopped + */ + stopCPU(fComplete) + { + var fStopped = false; + if (this.flags.running) { + this.endBurst(); + this.addCycles(this.nRunCycles); + this.nRunCycles = 0; + this.flags.running = false; + var controlRun = this.bindings["run"]; + if (controlRun) controlRun.textContent = "Run"; + if (this.cmp) { + this.cmp.stop(Component.getTime(), this.getCycles()); + } + fStopped = true; + if (!this.dbg) this.status("Stopped"); + } + this.flags.complete = fComplete; + return fStopped; + } + + /** + * yieldCPU() + * + * Similar to stopCPU() with regard to how it resets various cycle countdown values, but the CPU + * remains in a "running" state. + * + * @this {CPUPDP10} + */ + yieldCPU() + { + this.endBurst(); // this will break us out of stepCPU() + this.nCyclesNextYield = 0; // this will break us out of runCPU(), once we break out of stepCPU() + /* + * The Debugger calls yieldCPU() after every message() to ensure browser responsiveness, but it looks + * odd for those messages to show CPU state changes if the Control Panel, Video display, etc, does not, + * so I've added this call to try to keep things looking synchronized. + */ + this.updateDisplays(); + } +} + +/* + * Constants that control the frequency at which various updates should occur. + * + * These values do NOT control the simulation directly. Instead, they are used by + * calcCycles(), which uses the nCyclesPerSecond passed to the constructor as a starting + * point and computes the following variables: + * + * this.nCyclesPerYield: (this.nCyclesPerSecond / CPUPDP10.YIELDS_PER_SECOND) + * + * The above variables are also multiplied by any cycle multiplier in effect, via setSpeed(), + * and then they're used to initialize another set of variables for each runCPU() iteration: + * + * this.nCyclesNextYield: this.nCyclesPerYield + */ +CPUPDP10.YIELDS_PER_SECOND = 30; // just a gut feeling for the MINIMUM number of yields per second +CPUPDP10.YIELDS_PER_STATUS = 15; // every 15 yields (ie, twice per second), perform CPU status updates + +CPUPDP10.BUTTONS = ["power", "reset"]; + +if (NODE) module.exports = CPUPDP10; diff --git a/modules/pdp10/lib/cpustate.js b/modules/pdp10/lib/cpustate.js new file mode 100644 index 000000000..b5bf1e8e3 --- /dev/null +++ b/modules/pdp10/lib/cpustate.js @@ -0,0 +1,802 @@ +/** + * @fileoverview Implements the PDP-10 CPU component. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +if (NODE) { + var Str = require("../../shared/lib/strlib"); + var Web = require("../../shared/lib/weblib"); + var Component = require("../../shared/lib/component"); + var State = require("../../shared/lib/state"); + var PDP10 = require("./defines"); + var BusPDP10 = require("./bus"); + var CPUPDP10 = require("./cpu"); + var MessagesPDP10 = require("./messages"); + var MemoryPDP10 = require("./memory"); +} + +/* + * Overview of Device Interrupt Support + * + * Originally, the CPU maintained a queue of requested interrupts. Entries in this queue recorded a device's + * priority, vector, and delay (ie, a number of instructions to execute before dispatching the interrupt). This + * queue would constantly grow and shrink as requests were issued and dispatched, and as long as there was something + * in the queue, the CPU was constantly examining it. + * + * Now we are trying something more efficient. First, for devices that require delays (like the SerialPort's receiver + * and transmitter buffer registers, which are supposed to "clock" the data in and out at a specific baud rate), the + * CPU offers timer services that will "fire" a callback after a specified delay, which are much more efficient than + * requiring the CPU to dive into an interrupt queue and decrement delay counts on every instruction. + * + * Second, devices that generate interrupts will allocate an IRQ object during initialization; we will no longer + * be creating and destroying interrupt event objects and inserting/deleting them in a constantly changing queue. + * Each IRQ contains properties that never change (eg, the vector and priority), along with a "next" pointer that's + * only used when the IRQ is active. + * + * When a device decides it's time to interrupt (either at the end of some I/O operation or when a timer has fired), + * it will simply set the IRQ, which basically means that the IRQ will be linked onto a list of active IRQs, in + * priority order, so that when the CPU is ready to acknowledge interrupts, it need only check the top of the active + * IRQ list. + */ + +/** + * @typedef {{ + * vector: number, + * priority: number, + * message: number, + * name: (string|null), + * next: (IRQ|null) + * }} + */ +var IRQ; + +/** + * @class CPUStatePDP10 + * @unrestricted + */ +class CPUStatePDP10 extends CPUPDP10 { + /** + * CPUStatePDP10(parmsCPU) + * + * The CPUStatePDP10 class uses the following (parmsCPU) properties: + * + * model: a number (eg, 1001) that should match one of the PDP10.MODEL_* values + * addrReset: reset address (default is 0) + * + * This extends the CPU class and passes any remaining parmsCPU properties to the CPU class + * constructor, along with a default speed (cycles per second) based on the specified (or default) + * CPU model number. + * + * @param {Object} parmsCPU + */ + constructor(parmsCPU) + { + var nCyclesDefault = 0; + var model = +parmsCPU['model'] || PDP10.MODEL_KA10; + + switch(model) { + case PDP10.MODEL_KA10: + default: + nCyclesDefault = 6666667; + break; + } + + /* + * ES6 ALERT: Classes cannot access "this" until all superclasses have been initialized as well. + */ + super(parmsCPU, nCyclesDefault); + + this.model = model; + this.addrReset = +parmsCPU['addrReset'] || 0; + + /** @type {IRQ|null} */ + this.irqNext = null; // the head of the active IRQ list, in priority order + + /** @type {Array.} */ + this.aIRQs = []; // list of all IRQs, active or not (to be used for auto-configuration) + + this.flags.complete = false; + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * Called once the Bus has been initialized. + * + * @this {CPUStatePDP10} + * @param {ComputerPDP10} cmp + * @param {BusPDP10} bus + * @param {CPUPDP10} cpu + * @param {DebuggerPDP10} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + super.initBus(cmp, bus, cpu, dbg); + } + + /** + * reset() + * + * @this {CPUStatePDP10} + */ + reset() + { + this.status("Model " + this.model); + if (this.flags.running) this.stopCPU(); + this.initCPU(); + this.resetCycles(); + this.clearError(); // clear any fatal error/exception that setError() may have flagged + super.reset(); + } + + /** + * initCPU() + * + * @this {CPUStatePDP10} + */ + initCPU() + { + this.regPC = this.pcLast = this.addrReset; + + /* + * This is queried and displayed by the Panel when it's not displaying its own ADDRESS register + * (which takes precedence when, for example, you've manually halted the CPU and are independently + * examining the contents of other addresses). + * + * We initialize it to the current PC. + */ + this.addrLast = this.regPC; + + /* + * opFlags contains various conditions that stepCPU() needs to be aware of. + */ + this.opFlags = 0; + + this.setMemoryAccess(); + + this.resetIRQs(); + } + + /** + * setMemoryAccess() + * + * @this {CPUStatePDP10} + */ + setMemoryAccess() + { + this.readWord = this.readWordFromPhysical; + this.writeWord = this.writeWordToPhysical; + } + + /** + * setReset(addr, fStart, bUnit, addrStack) + * + * @this {CPUStatePDP10} + * @param {number} addr + * @param {boolean} [fStart] (true if a "startable" image was just loaded, false if not) + * @param {number} [bUnit] (boot unit #) + * @param {number} [addrStack] + */ + setReset(addr, fStart, bUnit, addrStack) + { + this.addrReset = addr; + + this.setPC(addr); + + if (fStart) { + if (!this.flags.powered) { + this.flags.autoStart = true; + } + else if (!this.flags.running) { + this.startCPU(); + } + } + else { + if (this.dbg && this.flags.powered) { + /* + * TODO: Review the decision to always stop the CPU if the Debugger is loaded. Note that + * when stopCPU() stops a running CPU, the Debugger gets notified, so no need to notify it again. + * + * TODO: There are more serious problems to deal with if another component is slamming a new PC down + * the CPU's throat (presumably while also dropping some new code into RAM) while the CPU is running; + * we should probably force a complete reset, but for now, it's up to the user to hit the reset button + * themselves. + */ + if (!this.stopCPU() && !this.cmp.flags.reset) { + this.dbg.updateStatus(); + this.cmp.updateDisplays(-1); + } + } + else if (fStart === false) { + this.stopCPU(); + } + } + if (!this.isRunning() && this.panel) this.panel.stop(); + } + + /** + * getChecksum() + * + * TODO: Implement + * + * @this {CPUStatePDP10} + * @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code) + */ + getChecksum() + { + return 0; + } + + /** + * save() + * + * @this {CPUStatePDP10} + * @return {Object|null} + */ + save() + { + var state = new State(this); + state.set(0, [ + this.regPC, + this.pcLast, + this.addrLast, + this.opFlags + ]); + state.set(1, []); + state.set(2, [this.nTotalCycles, this.getSpeed(), this.flags.autoStart]); + state.set(3, this.saveIRQs()); + state.set(4, this.saveTimers()); + return state.data(); + } + + /** + * restore(data) + * + * @this {CPUStatePDP10} + * @param {Object} data + * @return {boolean} true if restore successful, false if not + */ + restore(data) + { + /* + * ES6 ALERT: A handy destructuring assignment, which makes it easy to perform the inverse + * of what save() does when it collects a bunch of object properties into an array. + */ + [ + this.regPC, + this.pcLast, + this.addrLast, + this.opFlags + ] = data[0]; + + var a = data[2]; + this.nTotalCycles = a[0]; + this.setSpeed(a[1]); + this.flags.autoStart = a[2]; + + this.restoreIRQs(data[3]); + this.restoreTimers(data[4]); + return true; + } + + /** + * getOpcode() + * + * NOTE: This function is nothing more than a convenience, and we fully expect it to be inlined at runtime. + * + * @this {CPUStatePDP10} + * @return {number} + */ + getOpcode() + { + var pc = this.regPC; + var opCode = this.readWord(pc); + this.regPC = (pc + 1) % PDP10.ADDR_LIMIT; + return opCode; + } + + /** + * advancePC(off) + * + * NOTE: This function is nothing more than a convenience, and we fully expect it to be inlined at runtime. + * + * @this {CPUStatePDP10} + * @param {number} off + * @return {number} (original PC) + */ + advancePC(off) + { + var pc = this.regPC; + this.regPC = (pc + off) % PDP10.ADDR_LIMIT; + return pc; + } + + /** + * getPC() + * + * NOTE: This function is nothing more than a convenience, and we fully expect it to be inlined at runtime. + * + * @this {CPUStatePDP10} + * @return {number} + */ + getPC() + { + return this.regPC; + } + + /** + * getLastAddr() + * + * @this {CPUStatePDP10} + * @return {number} + */ + getLastAddr() + { + return this.addrLast; + } + + /** + * getLastPC() + * + * @this {CPUStatePDP10} + * @return {number} + */ + getLastPC() + { + return this.pcLast; + } + + /** + * setPC() + * + * NOTE: Unlike other PCjs emulators, such as PCx86, where all PC updates MUST go through the setPC() + * function, this function is nothing more than a convenience, because in the PDP-11, the PC can be loaded + * like any other general register. We fully expect this function to be inlined at runtime. + * + * @this {CPUStatePDP10} + * @param {number} addr + */ + setPC(addr) + { + this.regPC = addr % PDP10.ADDR_LIMIT; + } + + /** + * addIRQ(vector, priority, message) + * + * @this {CPUStatePDP10} + * @param {number} vector (-1 for floating vector) + * @param {number} priority + * @param {number} [message] + * @return {IRQ} + */ + addIRQ(vector, priority, message) + { + var irq = {vector: vector, priority: priority, message: message || 0, name: null, next: null}; + this.aIRQs.push(irq); + return irq; + } + + /** + * insertIRQ(irq) + * + * @this {CPUStatePDP10} + * @param {IRQ} irq + */ + insertIRQ(irq) + { + if (irq != this.irqNext) { + var irqPrev = this.irqNext; + if (!irqPrev || irqPrev.priority <= irq.priority) { + irq.next = irqPrev; + this.irqNext = irq; + } else { + do { + var irqNext = irqPrev.next; + if (!irqNext || irqNext.priority <= irq.priority) { + irq.next = irqNext; + irqPrev.next = irq; + break; + } + irqPrev = irqNext; + } while (irqPrev); + } + } + /* + * See the writeXCSR() function for an explanation of why signalling an IRQ hardware interrupt + * should be done using IRQ_DELAY rather than setting IRQ directly. + */ + this.opFlags |= PDP10.OPFLAG.IRQ_DELAY; + } + + /** + * removeIRQ(irq) + * + * @this {CPUStatePDP10} + * @param {IRQ} irq + */ + removeIRQ(irq) + { + var irqPrev = this.irqNext; + if (irqPrev == irq) { + this.irqNext = irq.next; + } else { + while (irqPrev) { + var irqNext = irqPrev.next; + if (irqNext == irq) { + irqPrev.next = irqNext.next; + break; + } + irqPrev = irqNext; + } + } + /* + * We could also set irq.next to null now, but strictly speaking, that shouldn't be necessary. + * + * Last but not least, if there's still an IRQ on the active IRQ list, we need to make sure IRQ_DELAY + * is still set. + */ + if (this.irqNext) { + this.opFlags |= PDP10.OPFLAG.IRQ_DELAY; + } + } + + /** + * setIRQ(irq) + * + * @this {CPUStatePDP10} + * @param {IRQ|null} irq + */ + setIRQ(irq) + { + if (irq) { + this.insertIRQ(irq); + if (irq.message && this.messageEnabled(irq.message | MessagesPDP10.INT)) { + this.printMessage("setIRQ(vector=" + Str.toOct(irq.vector) + ",priority=" + irq.priority + ")", true, true); + } + } + } + + /** + * clearIRQ(irq) + * + * @this {CPUStatePDP10} + * @param {IRQ|null} irq + */ + clearIRQ(irq) + { + if (irq) { + this.removeIRQ(irq); + if (irq.message && this.messageEnabled(irq.message | MessagesPDP10.INT)) { + this.printMessage("clearIRQ(vector=" + Str.toOct(irq.vector) + ",priority=" + irq.priority + ")", true, true); + } + } + } + + /** + * findIRQ(vector) + * + * @this {CPUStatePDP10} + * @param {number} vector + * @return {IRQ|null} + */ + findIRQ(vector) + { + for (var i = 0; i < this.aIRQs.length; i++) { + var irq = this.aIRQs[i]; + if (irq.vector === vector) return irq; + } + return null; + } + + /** + * checkIRQs(priority) + * + * @this {CPUStatePDP10} + * @param {number} priority + * @return {IRQ|null} + */ + checkIRQs(priority) + { + return (this.irqNext && this.irqNext.priority > priority)? this.irqNext : null; + } + + /** + * resetIRQs(priority) + * + * @this {CPUStatePDP10} + */ + resetIRQs() + { + this.irqNext = null; + } + + /** + * saveIRQs() + * + * @this {CPUStatePDP10} + * @return {Array.} + */ + saveIRQs() + { + var aIRQVectors = []; + var irq = this.irqNext; + while (irq) { + aIRQVectors.push(irq.vector); + irq = irq.next; + } + return aIRQVectors; + } + + /** + * restoreIRQs(aIRQVectors) + * + * @this {CPUStatePDP10} + * @param {Array.} aIRQVectors + */ + restoreIRQs(aIRQVectors) + { + for (var i = aIRQVectors.length - 1; i >= 0; i--) { + var irq = this.findIRQ(aIRQVectors[i]); + this.assert(irq != null); + if (irq) { + irq.next = this.irqNext; + this.irqNext = irq; + } + } + } + + /** + * checkInterrupts() + * + * @this {CPUStatePDP10} + * @return {boolean} true if an interrupt was dispatched, false if not + */ + checkInterrupts() + { + var fInterrupt = false; + + if (this.opFlags & PDP10.OPFLAG.IRQ) { + + // var vector = PDP10.TRAP.PIRQ; + // var priority = (this.regPIR & PDP10.PSW.PRI) >> PDP10.PSW.SHIFT.PRI; + // + // var irq = this.checkIRQs(priority); + // if (irq) { + // vector = irq.vector; + // priority = irq.priority; + // } + // + // if (this.dispatchInterrupt(vector, priority)) { + // if (irq) this.removeIRQ(irq); + // fInterrupt = true; + // } + + if (!this.irqNext) { + this.opFlags &= ~PDP10.OPFLAG.IRQ; + } + } + else if (this.opFlags & PDP10.OPFLAG.IRQ_DELAY) { + /* + * We know that IRQ (bit 2) is clear, so since IRQ_DELAY (bit 0) is set, incrementing opFlags + * will eventually transform IRQ_DELAY into IRQ, without affecting any other (higher) bits. + */ + this.opFlags++; + } + return fInterrupt; + } + + /** + * dispatchInterrupt(vector, priority) + * + * TODO: The process of dispatching an interrupt MUST cost some cycles; either trap() needs to assess + * that cost, or we do. + * + * @this {CPUStatePDP10} + * @param {number} vector + * @param {number} priority + * @return {boolean} (true if dispatched, false if not) + */ + dispatchInterrupt(vector, priority) + { + return false; + } + + /** + * isWaiting() + * + * @this {CPUStatePDP10} + * @return {boolean} (true if OPFLAG.WAIT is set, false otherwise) + */ + isWaiting() + { + return !!(this.opFlags & PDP10.OPFLAG.WAIT); + } + + /** + * readWordFromPhysical(addr) + * + * This is a handler set up by setMemoryAccess(). All calls should go through readWord(). + * + * @this {CPUStatePDP10} + * @param {number} addr + * @return {number} + */ + readWordFromPhysical(addr) + { + return this.bus.getWord(this.addrLast = addr); + } + + /** + * writeWordToPhysical(addr, data) + * + * This is a handler set up by setMemoryAccess(). All calls should go through writeWord(). + * + * @this {CPUStatePDP10} + * @param {number} addr + * @param {number} data + */ + writeWordToPhysical(addr, data) + { + this.bus.setWord(this.addrLast = addr, data); + } + + /** + * stepCPU(nMinCycles) + * + * NOTE: Single-stepping should not be confused with the Trap flag; single-stepping is a Debugger + * operation that's completely independent of Trap status. The CPU can go in and out of Trap mode, + * in and out of h/w interrupt service routines (ISRs), etc, but from the Debugger's perspective, + * they're all one continuous stream of instructions that can be stepped or run at will. Moreover, + * stepping vs. running should never change the behavior of the simulation. + * + * @this {CPUStatePDP10} + * @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored) + * @return {number} of cycles executed; 0 indicates a pre-execution condition (ie, an execution breakpoint + * was hit), -1 indicates a post-execution condition (eg, a read or write breakpoint was hit), and a positive + * number indicates successful completion of that many cycles (which should always be >= nMinCycles). + */ + stepCPU(nMinCycles) + { + /* + * The Debugger uses complete to determine if the instruction completed (true) or was interrupted + * by a breakpoint or some other exceptional condition (false). NOTE: this does NOT include JavaScript + * exceptions, which stepCPU() expects the caller to catch using its own exception handler. + * + * The CPU relies on the use of stopCPU() rather than complete, because the CPU never single-steps + * (ie, nMinCycles is always some large number), whereas the Debugger does. And conversely, when the + * Debugger is single-stepping (even when performing multiple single-steps), fRunning is never set, + * so stopCPU() would have no effect as far as the Debugger is concerned. + */ + this.flags.complete = true; + + /* + * nDebugCheck is 1 if we want the Debugger's checkInstruction() to check every instruction, + * -1 if we want it to check just the first instruction, and 0 if there's no need for any checks. + */ + var nDebugCheck = (DEBUGGER && this.dbg)? (this.dbg.checksEnabled()? 1 : (this.flags.starting? -1 : 0)) : 0; + + /* + * nDebugState is needed only when nDebugCheck is non-zero; it is -1 if this is a single-step, 0 if + * this is the start of a new run, and 1 if this is a continuation of a previous run. It is used by + * checkInstruction() to determine if it should skip breakpoint checks and/or HALT instructions (ie, + * if nDebugState is <= zero). + */ + var nDebugState = (!nMinCycles)? -1 : (this.flags.starting? 0 : 1); + this.flags.starting = false; // we've moved beyond "starting" and have officially "started" now + + /* + * We move the minimum cycle count to nStepCycles (the number of cycles left to step), so that other + * functions have the ability to force that number to zero (eg, stopCPU()), and thus we don't have to check + * any other criteria to determine whether we should continue stepping or not. + */ + this.nBurstCycles = this.nStepCycles = nMinCycles; + + /* + * And finally, move the nDebugCheck state to an OPFLAG bit, so that the loop need check only one variable. + */ + this.opFlags = (this.opFlags & ~PDP10.OPFLAG.DEBUGGER) | (nDebugCheck? PDP10.OPFLAG.DEBUGGER : 0); + + do { + if (this.opFlags) { + /* + * NOTE: We still check DEBUGGER to ensure that this code will be compiled out of existence in + * non-DEBUGGER builds. + */ + if (DEBUGGER && (this.opFlags & PDP10.OPFLAG.DEBUGGER)) { + if (this.dbg.checkInstruction(this.getPC(), nDebugState)) { + this.stopCPU(); + break; + } + if (!++nDebugCheck) this.opFlags &= ~PDP10.OPFLAG.DEBUGGER; + if (!nDebugState) nDebugState++; + } + /* + * If we're in the IRQ or WAIT state, check for any pending interrupts. + * + * NOTE: It's no coincidence that we're checking this BEFORE any pending traps, because in rare + * cases (including some presented by those pesky "TRAP TEST" diagnostics), the process of dispatching + * an interrupt can trigger a TRAP_SP stack overflow condition, which must be dealt with BEFORE we + * execute the first instruction of the interrupt handler. + */ + if ((this.opFlags & (PDP10.OPFLAG.IRQ_MASK | PDP10.OPFLAG.WAIT)) /* && nDebugState >= 0 */) { + if (this.checkInterrupts()) { + if ((this.opFlags & PDP10.OPFLAG.DEBUGGER) && this.dbg.checkInstruction(this.getPC(), nDebugState)) { + this.stopCPU(); + break; + } + /* + * Since an interrupt was just dispatched, altering the normal flow of time and changing + * the future as we knew it, let's break out immediately if we're single-stepping, so that + * the Debugger gets to see the first instruction of the interrupt handler. NOTE: This + * assumes that we've still commented out the nDebugState check above that used to bypass + * checkInterrupts() when single-stepping. + */ + if (nDebugState < 0) break; + } + } + } + + this.opFlags &= PDP10.OPFLAG.PRESERVE; + + var opCode = this.getOpcode(); + + // this.decode(opCode); + + } while (this.nStepCycles > 0); + + return (this.flags.complete? this.nBurstCycles - this.nStepCycles : (this.flags.complete === false? -1 : 0)); + } + + /** + * CPUStatePDP10.init() + * + * This function operates on every HTML element of class "cpu", extracting the + * JSON-encoded parameters for the CPUStatePDP10 constructor from the element's "data-value" + * attribute, invoking the constructor (which in turn invokes the CPU constructor) + * to create a CPUStatePDP10 component, and then binding any associated HTML controls to the + * new component. + */ + static init() + { + var aeCPUs = Component.getElementsByClass(document, PDP10.APPCLASS, "cpu"); + for (var iCPU = 0; iCPU < aeCPUs.length; iCPU++) { + var eCPU = aeCPUs[iCPU]; + var parmsCPU = Component.getComponentParms(eCPU); + var cpu = new CPUStatePDP10(parmsCPU); + Component.bindComponentControls(cpu, eCPU, PDP10.APPCLASS); + } + } +} + +/* + * Initialize every CPU module on the page + */ +Web.onInit(CPUStatePDP10.init); + +if (NODE) module.exports = CPUStatePDP10; diff --git a/modules/pdp10/lib/debugger.js b/modules/pdp10/lib/debugger.js new file mode 100644 index 000000000..7d04e1a7d --- /dev/null +++ b/modules/pdp10/lib/debugger.js @@ -0,0 +1,3558 @@ +/** + * @fileoverview Implements the PDP-10 Debugger component. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +if (NODE) { + var Str = require("../../shared/lib/strlib"); + var Usr = require("../../shared/lib/usrlib"); + var Web = require("../../shared/lib/weblib"); + var Component = require("../../shared/lib/component"); + var Debugger = require("../../shared/lib/debugger"); + var Keys = require("../../shared/lib/keys"); + var State = require("../../shared/lib/state"); + var PDP10 = require("./defines"); + var BusPDP10 = require("./bus"); + var MemoryPDP10 = require("./memory"); + var MessagesPDP10 = require("./messages"); +} + +/** + * DebuggerPDP10 Address Object + * + * addr address + * fPhysical true if this is a physical address + * fTemporary true if this is a temporary breakpoint address + * nBase set if the address contained an explicit base (eg, 16, 10, 8, etc) + * sCmd set for breakpoint addresses if there's an associated command string + * aCmds preprocessed commands (from sCmd) + * + * @typedef {{ + * addr:(number|null), + * fPhysical:(boolean), + * fTemporary:(boolean), + * nBase:(number|undefined), + * sCmd:(string|undefined), + * aCmds:(Array.|undefined) + * }} + */ +var DbgAddrPDP10; + +class DebuggerPDP10 extends Debugger { + /** + * DebuggerPDP10(parmsDbg) + * + * The DebuggerPDP10 component supports the following optional (parmsDbg) properties: + * + * commands: string containing zero or more commands, separated by ';' + * + * messages: string containing zero or more message categories to enable; + * multiple categories must be separated by '|' or ';'. Parsed by messageInit(). + * + * The DebuggerPDP10 component is an optional component that implements a variety of user + * commands for controlling the CPU, dumping and editing memory, etc. + * + * @param {Object} parmsDbg + */ + constructor(parmsDbg) + { + if (DEBUGGER) { + + super(parmsDbg); + + /* + * Since this Debugger doesn't use replaceRegs(), we can use parentheses instead of braces. + */ + this.fInit = false; + this.fParens = true; + + /* + * Most commands that require an address call parseAddr(), which defaults to dbgAddrNextCode + * or dbgAddrNextData when no address has been given. doDump() and doUnassemble(), in turn, + * update dbgAddrNextData and dbgAddrNextCode, respectively, when they're done. + * + * For TEMPORARY breakpoint addresses, we set fTemporary to true, so that they can be automatically + * cleared when they're hit. + */ + this.dbgAddrNextCode = this.newAddr(); + this.dbgAddrNextData = this.newAddr(); + this.dbgAddrAssemble = this.newAddr(); + + /* + * aSymbolTable is an array of SymbolTable objects, one per ROM or other chunk of address space, + * where each object contains the following properties: + * + * sModule + * addr (physical address, if any; eg, symbols for a ROM) + * len + * aSymbols + * aOffsets + * + * See addSymbols() for more details, since that's how callers add sets of symbols to the table. + */ + this.aSymbolTable = []; + + /* + * clearBreakpoints() initializes the breakpoints lists: aBreakExec is a list of addresses + * to halt on whenever attempting to execute an instruction at the corresponding address, + * and aBreakRead and aBreakWrite are lists of addresses to halt on whenever a read or write, + * respectively, occurs at the corresponding address. + * + * NOTE: Curiously, after upgrading the Google Closure Compiler from v20141215 to v20150609, + * the resulting compiled code would crash in clearBreakpoints(), because the (renamed) aBreakRead + * property was already defined. To eliminate whatever was confusing the Closure Compiler, I've + * explicitly initialized all the properties that clearBreakpoints() (re)initializes. + */ + this.aBreakExec = this.aBreakRead = this.aBreakWrite = []; + this.clearBreakpoints(); + + /* + * The new "bn" command allows you to specify a number of instructions to execute and then stop; + * "bn 0" disables any outstanding count. + */ + this.nBreakInstructions = 0; + + /* + * Execution history is allocated by historyInit() whenever checksEnabled() conditions change. + * Execution history is updated whenever the CPU calls checkInstruction(), which will happen + * only when checksEnabled() returns true (eg, whenever one or more breakpoints have been set). + * This ensures that, by default, the CPU runs as fast as possible. + */ + this.iInstructionHistory = 0; + this.aInstructionHistory = []; + this.nextHistory = undefined; + this.historyInit(); + + /* + * Initialize DebuggerPDP10 message support. + */ + this.dbg = this; + this.afnDumpers = {}; + this.bitsMessage = this.bitsWarning = 0; + this.sMessagePrev = null; + this.aMessageBuffer = []; + this.messageInit(parmsDbg['messages']); + this.sInitCommands = parmsDbg['commands']; + + /* + * Define remaining miscellaneous DebuggerPDP10 properties. + */ + this.opTable = DebuggerPDP10.OPTABLE; + this.aOpReserved = []; + this.nStep = 0; + this.sCmdTracePrev = null; + this.sCmdDumpPrev = null; + this.fIgnoreNextCheckFault = false; // TODO: Does this serve any purpose on a PDP-11? + this.nSuppressBreaks = 0; + this.cInstructions = this.cInstructionsStart = 0; + this.nCycles = this.nCyclesStart = this.msStart = 0; + this.controlDebug = null; + this.panel = null; + + /* + * Make it easier to access DebuggerPDP10 commands from an external REPL (eg, the WebStorm + * "live" console window); eg: + * + * pdp10('r') + * pdp10('dw 0:0') + * pdp10('h') + * ... + */ + var dbg = this; + if (window) { + if (window[PDP10.APPCLASS] === undefined) { + window[PDP10.APPCLASS] = function(s) { return dbg.doCommands(s); }; + } + } else { + if (global[PDP10.APPCLASS] === undefined) { + global[PDP10.APPCLASS] = function(s) { return dbg.doCommands(s); }; + } + } + + } // endif DEBUGGER + } + + /** + * getAddr(dbgAddr, fWrite) + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10|null} [dbgAddr] + * @param {boolean} [fWrite] + * @return {number} is the corresponding linear address, or PDP10.ADDR_INVALID + */ + getAddr(dbgAddr, fWrite) + { + var addr = dbgAddr && dbgAddr.addr; + if (addr == null) addr = PDP10.ADDR_INVALID; + return addr; + } + + /** + * newAddr(addr, fPhysical, nBase) + * + * Returns a NEW DbgAddrPDP10 object, initialized with specified values and/or defaults. + * + * @this {DebuggerPDP10} + * @param {number|null} [addr] + * @param {boolean} [fPhysical] + * @param {number} [nBase] + * @return {DbgAddrPDP10} + */ + newAddr(addr = null, fPhysical = false, nBase) + { + return {addr: addr, fPhysical: fPhysical, fTemporary: false, nBase: nBase}; + } + + /** + * setAddr(dbgAddr, addr) + * + * Updates an EXISTING DbgAddrPDP10 object, initialized with specified values and/or defaults. + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10} dbgAddr + * @param {number} addr + * @return {DbgAddrPDP10} + */ + setAddr(dbgAddr, addr) + { + dbgAddr.addr = addr; + dbgAddr.fTemporary = false; + dbgAddr.nBase = undefined; + return dbgAddr; + } + + /** + * packAddr(dbgAddr) + * + * Packs a DbgAddrPDP10 object into an Array suitable for saving in a machine state object. + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10} dbgAddr + * @return {Array} + */ + packAddr(dbgAddr) + { + return [dbgAddr.addr, dbgAddr.fPhysical, dbgAddr.nBase, dbgAddr.fTemporary, dbgAddr.sCmd]; + } + + /** + * unpackAddr(aAddr) + * + * Unpacks a DbgAddrPDP10 object from an Array created by packAddr() and restored from a saved machine state. + * + * @this {DebuggerPDP10} + * @param {Array} aAddr + * @return {DbgAddrPDP10} + */ + unpackAddr(aAddr) + { + var dbgAddr = this.newAddr(aAddr[0], aAddr[1], aAddr[2]); + dbgAddr.fTemporary = aAddr[3]; + if (aAddr[4]) { + dbgAddr.aCmds = this.parseCommand(dbgAddr.sCmd = aAddr[4]); + } + return dbgAddr; + } + + /** + * initBus(bus, cpu, dbg) + * + * @this {DebuggerPDP10} + * @param {ComputerPDP10} cmp + * @param {BusPDP10} bus + * @param {CPUStatePDP10} cpu + * @param {DebuggerPDP10} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.bus = bus; + this.cmp = cmp; + this.cpu = cpu; + this.panel = cmp.panel; + + /* + * Re-initialize Debugger message support if necessary + */ + var sMessages = /** @type {string|undefined} */ (cmp.getMachineParm('messages')); + if (sMessages) this.messageInit(sMessages); + + /* + * Update aOpReserved as appropriate for the current model + */ + + this.messageDump(MessagesPDP10.BUS, function onDumpBus(asArgs) { dbg.dumpBus(asArgs); }); + + this.setReady(); + } + + /** + * setBinding(sType, sBinding, control, sValue) + * + * @this {DebuggerPDP10} + * @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc) + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "debugInput") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sType, sBinding, control, sValue) + { + var dbg = this; + switch (sBinding) { + + case "debugInput": + this.bindings[sBinding] = control; + this.controlDebug = control; + /* + * For halted machines, this is fine, but for auto-start machines, it can be annoying. + * + * control.focus(); + */ + control.onkeydown = function onKeyDownDebugInput(event) { + var sCmd; + if (event.keyCode == Keys.KEYCODE.CR) { + sCmd = control.value; + control.value = ""; + dbg.doCommands(sCmd, true); + } + else if (event.keyCode == Keys.KEYCODE.ESC) { + control.value = sCmd = ""; + } + else { + if (event.keyCode == Keys.KEYCODE.UP) { + sCmd = dbg.getPrevCommand(); + } + else if (event.keyCode == Keys.KEYCODE.DOWN) { + sCmd = dbg.getNextCommand(); + } + if (sCmd != null) { + var cch = sCmd.length; + control.value = sCmd; + control.setSelectionRange(cch, cch); + } + } + if (sCmd != null && event.preventDefault) event.preventDefault(); + }; + return true; + + case "debugEnter": + this.bindings[sBinding] = control; + Web.onClickRepeat( + control, + 500, 100, + function onClickDebugEnter(fRepeat) { + if (dbg.controlDebug) { + var sCmd = dbg.controlDebug.value; + dbg.controlDebug.value = ""; + dbg.doCommands(sCmd, true); + return true; + } + if (DEBUG) dbg.log("no debugger input buffer"); + return false; + } + ); + return true; + + case "step": + this.bindings[sBinding] = control; + Web.onClickRepeat( + control, + 500, 100, + function onClickStep(fRepeat) { + var fCompleted = false; + if (!dbg.isBusy(true)) { + dbg.setBusy(true); + fCompleted = dbg.stepCPU(fRepeat? 1 : 0, null); + dbg.setBusy(false); + } + return fCompleted; + } + ); + return true; + + default: + break; + } + return false; + } + + /** + * setFocus(fScroll) + * + * @this {DebuggerPDP10} + * @param {boolean} [fScroll] (true if you really want the control scrolled into view) + */ + setFocus(fScroll) + { + if (this.controlDebug) { + /* + * This is the recommended work-around to prevent the browser from scrolling the focused element + * into view. The CPU is not a visual component, so when the CPU wants to set focus, the primary intent + * is to ensure that keyboard input is fielded properly. + */ + var x = 0, y = 0; + if (!fScroll && window) { + x = window.scrollX; + y = window.scrollY; + } + + this.controlDebug.focus(); + + if (!fScroll && window) { + window.scrollTo(x, y); + } + } + } + + /** + * getWord(dbgAddr, inc) + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10} dbgAddr + * @param {number} [inc] + * @return {number} + */ + getWord(dbgAddr, inc) + { + var w = PDP10.DATA_INVALID; + var addr = this.getAddr(dbgAddr, false); + if (addr !== PDP10.ADDR_INVALID) { + w = this.bus.getWordDirect(addr); + if (inc) this.incAddr(dbgAddr, inc); + } + return w; + } + + /** + * setWord(dbgAddr, w, inc) + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10} dbgAddr + * @param {number} w + * @param {number} [inc] + */ + setWord(dbgAddr, w, inc) + { + var addr = this.getAddr(dbgAddr, true); + if (addr !== PDP10.ADDR_INVALID) { + this.bus.setWordDirect(addr, w); + if (inc) this.incAddr(dbgAddr, inc); + this.cmp.updateDisplays(-1); + } + } + + /** + * parseAddr(sAddr, fCode, fNoChecks, fPrint) + * + * Address evaluation and validation (eg, range checks) are no longer performed at this stage. That's + * done later, by getAddr(), which returns PDP10.ADDR_INVALID for invalid segments, out-of-range offsets, + * etc. The Debugger's low-level get/set memory functions verify all getAddr() results, but even if an + * invalid address is passed through to the Bus memory interfaces, the address will simply be masked with + * bus.nBusMask; in the case of PDP10.ADDR_INVALID, that will generally refer to the top of the physical + * address space. + * + * @this {DebuggerPDP10} + * @param {string|undefined} sAddr + * @param {boolean} [fCode] (true if target is code, false if target is data) + * @param {boolean} [fNoChecks] (true when setting breakpoints that may not be valid now, but will be later) + * @param {boolean} [fPrint] + * @return {DbgAddrPDP10|null|undefined} + */ + parseAddr(sAddr, fCode, fNoChecks, fPrint) + { + var dbgAddr; + var dbgAddrNext = (fCode? this.dbgAddrNextCode : this.dbgAddrNextData); + var addr = dbgAddrNext.addr; + var fPhysical, nBase; + if (sAddr !== undefined) { + sAddr = this.parseReference(sAddr); + var ch = sAddr.charAt(0); + if (ch == '%') { + fPhysical = true; + sAddr = sAddr.substr(1); + } + dbgAddr = this.findSymbolAddr(sAddr); + if (dbgAddr) return dbgAddr; + if (sAddr.indexOf("0x") >= 0) { + nBase = 16 + } else if (sAddr.indexOf("0o") >= 0) { + nBase = 8; + } else if (sAddr.indexOf('.') >= 0) { + nBase = 10; + } + addr = this.parseExpression(sAddr, fPrint); + } + if (addr != null) { + dbgAddr = this.newAddr(addr, fPhysical, nBase); + } + return dbgAddr; + } + + /** + * parseAddrOptions(dbdAddr, sOptions) + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10} dbgAddr + * @param {string} [sOptions] + */ + parseAddrOptions(dbgAddr, sOptions) + { + if (sOptions) { + var a = sOptions.match(/(['"])(.*?)\1/); + if (a) { + dbgAddr.aCmds = this.parseCommand(dbgAddr.sCmd = a[2]); + } + } + } + + /** + * incAddr(dbgAddr, inc) + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10} dbgAddr + * @param {number} [inc] contains value to increment dbgAddr by (default is 1) + */ + incAddr(dbgAddr, inc) + { + if (dbgAddr.addr != null) { + dbgAddr.addr += (inc || 1); + } + } + + /** + * toStrOffset(off) + * + * @this {DebuggerPDP10} + * @param {number|null|undefined} [off] + * @return {string} the hex representation of off + */ + toStrOffset(off) + { + return this.toStrBase(off); + } + + /** + * toStrAddr(dbgAddr) + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10} dbgAddr + * @return {string} the hex representation of the address + */ + toStrAddr(dbgAddr) + { + return this.toStrOffset(dbgAddr.addr); + } + + /** + * dumpBlocks(aBlocks, sAddr) + * + * @this {DebuggerPDP10} + * @param {Array} aBlocks + * @param {string} [sAddr] (optional block address) + */ + dumpBlocks(aBlocks, sAddr) + { + var addr = 0, i = 0, n = aBlocks.length; + + if (sAddr) { + addr = this.getAddr(this.parseAddr(sAddr)); + if (addr === PDP10.ADDR_INVALID) { + this.println("invalid address: " + sAddr); + return; + } + i = addr >>> this.bus.nBlockShift; + n = 1; + } + + this.println("blockid physical blockaddr used size type"); + this.println("-------- --------- --------- ------ ------ ----"); + + var typePrev = -1, cPrev = 0; + while (n--) { + var block = aBlocks[i]; + if (block.type == typePrev) { + if (!cPrev++) this.println("..."); + } else { + typePrev = block.type; + var sType = MemoryPDP10.TYPE_NAMES[typePrev]; + if (block) { + this.println(Str.toHex(block.id, 8) + " %" + Str.toHex(i << this.bus.nBlockShift, 8) + " %" + Str.toHex(block.addr, 8) + " " + Str.toHexWord(block.used) + " " + Str.toHexWord(block.size) + " " + sType); + } + if (typePrev != MemoryPDP10.TYPE.NONE) typePrev = -1; + cPrev = 0; + } + addr += this.bus.nBlockSize; + i++; + } + } + + /** + * dumpBus(asArgs) + * + * Dumps Bus allocations. + * + * @this {DebuggerPDP10} + * @param {Array.} asArgs (asArgs[0] is an optional block address) + */ + dumpBus(asArgs) + { + this.dumpBlocks(this.bus.aBusBlocks, asArgs[0]); + } + + /** + * dumpHistory(sPrev, sLines) + * + * If sLines is not a number, it can be a instruction filter. However, for the moment, the only + * supported filter is "call", which filters the history buffer for all CALL and RET instructions + * from the specified previous point forward. + * + * @this {DebuggerPDP10} + * @param {string} [sPrev] is a (decimal) number of instructions to rewind to (default is 10) + * @param {string} [sLines] is a (decimal) number of instructions to print (default is, again, 10) + */ + dumpHistory(sPrev, sLines) + { + var sMore = ""; + var cHistory = 0; + var iHistory = this.iInstructionHistory; + var aHistory = this.aInstructionHistory; + + if (aHistory.length) { + var nPrev = +sPrev || this.nextHistory; + var nLines = +sLines || 10; + + if (isNaN(nPrev)) { + nPrev = nLines; + } else { + sMore = "more "; + } + + if (nPrev > aHistory.length) { + this.println("note: only " + aHistory.length + " available"); + nPrev = aHistory.length; + } + + iHistory -= nPrev; + if (iHistory < 0) { + /* + * If the dbgAddr of the last aHistory element contains a valid selector, wrap around. + */ + if (aHistory[aHistory.length - 1].addr == null) { + nPrev = iHistory + nPrev; + iHistory = 0; + } else { + iHistory += aHistory.length; + } + } + + var aFilters = []; + if (sLines == "call") { + nLines = 100000; + aFilters = ["CALL"]; + } + + if (sPrev !== undefined) { + this.println(nPrev + " instructions earlier:"); + } + + /* + * TODO: The following is necessary to prevent dumpHistory() from causing additional (or worse, recursive) + * faults due to segmented addresses that are no longer valid, but the only alternative is to dramatically + * increase the amount of memory used to store instruction history (eg, storing copies of all the instruction + * bytes alongside the execution addresses). + * + * For now, we're living dangerously, so that our history dumps actually work. + * + * this.nSuppressBreaks++; + * + * If you re-enable this protection, be sure to re-enable the decrement below, too. + */ + while (nLines > 0 && iHistory != this.iInstructionHistory) { + + var dbgAddr = aHistory[iHistory++]; + if (dbgAddr.addr == null) break; + + /* + * We must create a new dbgAddr from the address in aHistory, because dbgAddr was + * a reference, not a copy, and we don't want getInstruction() modifying the original. + */ + var dbgAddrNew = this.newAddr(dbgAddr.addr); + + var sComment = "history"; + var nSequence = nPrev--; + + /* + * TODO: Need to some UI to control whether cycle counts are displayed as part of the history. + * It's currently disabled in checkInstruction(), so it's disable here, too. + * + if (DEBUG && dbgAddr.cycleCount != null) { + sComment = "cycles"; + nSequence = dbgAddr.cycleCount; + } + */ + + var sInstruction = this.getInstruction(dbgAddrNew, sComment, nSequence); + + if (!aFilters.length || sInstruction.indexOf(aFilters[0]) >= 0) { + this.println(sInstruction); + } + + /* + * If there were OPERAND or ADDRESS overrides on the previous instruction, getInstruction() + * will have automatically disassembled additional bytes, so skip additional history entries. + */ + if (dbgAddrNew.cOverrides) { + iHistory += dbgAddrNew.cOverrides; nLines -= dbgAddrNew.cOverrides; nPrev -= dbgAddrNew.cOverrides; + } + + if (iHistory >= aHistory.length) iHistory = 0; + this.nextHistory = nPrev; + cHistory++; + nLines--; + } + /* + * See comments above. + * + * this.nSuppressBreaks--; + */ + } + + if (!cHistory) { + this.println("no " + sMore + "history available"); + this.nextHistory = undefined; + } + } + + /** + * messageInit(sEnable) + * + * @this {DebuggerPDP10} + * @param {string|undefined} sEnable contains zero or more message categories to enable, separated by '|' + */ + messageInit(sEnable) + { + this.dbg = this; + this.bitsMessage = this.bitsWarning = MessagesPDP10.WARN; + this.sMessagePrev = null; + this.aMessageBuffer = []; + /* + * Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects, + * but externally, we allow the user to specify "keys"; "kbd" is also allowed as shorthand for "keyboard". + */ + var aEnable = this.parseCommand(sEnable.replace("keys","key").replace("kbd","keyboard"), false, '|'); + if (aEnable.length) { + for (var m in MessagesPDP10.CATEGORIES) { + if (Usr.indexOf(aEnable, m) >= 0) { + this.bitsMessage |= MessagesPDP10.CATEGORIES[m]; + this.println(m + " messages enabled"); + } + } + } + } + + /** + * messageDump(bitMessage, fnDumper) + * + * @this {DebuggerPDP10} + * @param {number} bitMessage is one Messages category flag + * @param {function(Array.)} fnDumper is a function the Debugger can use to dump data for that category + * @return {boolean} true if successfully registered, false if not + */ + messageDump(bitMessage, fnDumper) + { + for (var m in MessagesPDP10.CATEGORIES) { + if (bitMessage == MessagesPDP10.CATEGORIES[m]) { + this.afnDumpers[m] = fnDumper; + return true; + } + } + return false; + } + + /** + * getRegIndex(sReg, off) + * + * @this {DebuggerPDP10} + * @param {string} sReg + * @param {number} [off] optional offset into sReg + * @return {number} register index, or -1 if not found + */ + getRegIndex(sReg, off) + { + return DebuggerPDP10.REGNAMES.indexOf(sReg.toUpperCase()); + } + + /** + * getRegName(iReg) + * + * @this {DebuggerPDP10} + * @param {number} iReg (0-7; not used for other registers) + * @return {string|undefined} + */ + getRegName(iReg) + { + return DebuggerPDP10.REGNAMES[iReg]; + } + + /** + * getRegValue(iReg) + * + * @this {DebuggerPDP10} + * @param {number} iReg + * @return {number|undefined} + */ + getRegValue(iReg) + { + var value; + switch(iReg) { + case DebuggerPDP10.REGS.PC: + value = this.cpu.getPC(); + break; + } + return value; + } + + /** + * replaceRegs(s) + * + * TODO: Implement or eliminate. + * + * @this {DebuggerPDP10} + * @param {string} s + * @return {string} + */ + replaceRegs(s) + { + return s; + } + + /** + * message(sMessage, fAddress) + * + * @this {DebuggerPDP10} + * @param {string} sMessage is any caller-defined message string + * @param {boolean} [fAddress] is true to display the current address + */ + message(sMessage, fAddress) + { + if (fAddress) { + sMessage += " @" + this.toStrAddr(this.newAddr(this.cpu.getLastPC())); + } + + if (this.sMessagePrev && sMessage == this.sMessagePrev) return; + this.sMessagePrev = sMessage; + + if (this.bitsMessage & MessagesPDP10.BUFFER) { + this.aMessageBuffer.push(sMessage); + return; + } + + var fRunning; + if ((this.bitsMessage & MessagesPDP10.HALT) && this.cpu && (fRunning = this.cpu.isRunning()) || this.isBusy(true)) { + this.stopCPU(); + if (fRunning) sMessage += " (cpu halted)"; + } + + this.println(sMessage); // + " (" + this.cpu.getCycles() + " cycles)" + + /* + * We have no idea what the frequency of println() calls might be; all we know is that they easily + * screw up the CPU's careful assumptions about cycles per burst. So we call yieldCPU() after every + * message, to effectively end the current burst and start fresh. + * + * TODO: See CPUPDP10.calcStartTime() for a discussion of why we might want to call yieldCPU() *before* + * we display the message. + */ + if (this.cpu) this.cpu.yieldCPU(); + } + + /** + * init() + * + * @this {DebuggerPDP10} + * @param {boolean} [fAutoStart] + */ + init(fAutoStart) + { + this.fInit = true; + this.println("Type ? for help with PDPjs Debugger commands"); + this.updateStatus(); + if (!fAutoStart) this.setFocus(); + if (this.sInitCommands) { + var sCmds = this.sInitCommands; + this.sInitCommands = null; + this.doCommands(sCmds); + } + } + + /** + * historyInit(fQuiet) + * + * This function is intended to be called by the constructor, reset(), addBreakpoint(), findBreakpoint() + * and any other function that changes the checksEnabled() criteria used to decide whether checkInstruction() + * should be called. + * + * That is, if the history arrays need to be allocated and haven't already been allocated, then allocate them, + * and if the arrays are no longer needed, then deallocate them. + * + * @this {DebuggerPDP10} + * @param {boolean} [fQuiet] + */ + historyInit(fQuiet) + { + var i; + if (!this.checksEnabled()) { + if (this.aInstructionHistory && this.aInstructionHistory.length && !fQuiet) { + this.println("instruction history buffer freed"); + } + this.iInstructionHistory = 0; + this.aInstructionHistory = []; + return; + } + if (!this.aInstructionHistory || !this.aInstructionHistory.length) { + this.aInstructionHistory = new Array(DebuggerPDP10.HISTORY_LIMIT); + for (i = 0; i < this.aInstructionHistory.length; i++) { + /* + * Preallocate dummy Addr (Array) objects in every history slot, so that + * checkInstruction() doesn't need to call newAddr() on every slot update. + */ + this.aInstructionHistory[i] = this.newAddr(); + } + this.iInstructionHistory = 0; + if (!fQuiet) { + this.println("instruction history buffer allocated"); + } + } + } + + /** + * startCPU(fUpdateFocus, fQuiet) + * + * @this {DebuggerPDP10} + * @param {boolean} [fUpdateFocus] is true to update focus + * @param {boolean} [fQuiet] + * @return {boolean} true if run request successful, false if not + */ + startCPU(fUpdateFocus, fQuiet) + { + if (!this.checkCPU(fQuiet)) return false; + this.cpu.startCPU(fUpdateFocus); + return true; + } + + /** + * stepCPU(nCycles, fRegs, fUpdateDisplays) + * + * @this {DebuggerPDP10} + * @param {number} nCycles (0 for one instruction without checking breakpoints) + * @param {boolean|null} [fRegs] is true to display registers after step (default is false; use null for previous setting) + * @param {boolean} [fUpdateDisplays] is false to disable Computer display updates (default is true) + * @return {boolean} + */ + stepCPU(nCycles, fRegs, fUpdateDisplays) + { + if (!this.checkCPU()) return false; + + var sCmd = ""; + if (fRegs === null) { + fRegs = (!this.sCmdTracePrev || this.sCmdTracePrev == "tr"); + sCmd = fRegs? "tr" : "t"; + } + + this.nCycles = 0; + + if (!nCycles) { + /* + * When single-stepping, the CPU won't call checkInstruction(), which is good for + * avoiding breakpoints, but bad for instruction data collection if checks are enabled. + * So we call checkInstruction() ourselves. + */ + if (this.checksEnabled()) this.checkInstruction(this.cpu.getPC(), 0); + } + /* + * For our typically tiny bursts (usually single instructions), mimic what runCPU() does. + */ + try { + nCycles = this.cpu.getBurstCycles(nCycles); + var nCyclesStep = this.cpu.stepCPU(nCycles); + if (nCyclesStep > 0) { + this.cpu.updateTimers(nCyclesStep); + this.nCycles += nCyclesStep; + this.cpu.addCycles(nCyclesStep, true); + this.cpu.updateChecksum(nCyclesStep); + this.cInstructions++; + } + } + catch(exception) { + /* + * We assume that any numeric exception was explicitly thrown by the CPU to interrupt the + * current instruction. For all other exceptions, we attempt a stack dump. + */ + if (typeof exception != "number") { + var e = exception; + this.nCycles = 0; + this.cpu.setError(e.stack || e.message); + } + } + + /* + * Because we called cpu.stepCPU() and not cpu.startCPU(), we must nudge the Computer's update code, + * and then update our own state. Normally, the only time fUpdateDisplays will be false is when doTrace() + * is calling us in a loop, in which case it will perform its own updateDisplays() when it's done. + */ + if (fUpdateDisplays !== false) { + if (this.panel) this.panel.stop(); + this.cmp.updateDisplays(-1); + } + + this.updateStatus(fRegs || false, sCmd); + return (this.nCycles > 0); + } + + /** + * stopCPU() + * + * @this {DebuggerPDP10} + * @param {boolean} [fComplete] + */ + stopCPU(fComplete) + { + if (this.cpu) this.cpu.stopCPU(fComplete); + } + + /** + * updateStatus(fRegs, sCmd) + * + * @this {DebuggerPDP10} + * @param {boolean} [fRegs] (default is true) + * @param {string} [sCmd] + */ + updateStatus(fRegs, sCmd) + { + if (!this.fInit) return; + + if (fRegs === undefined) fRegs = true; + + if (sCmd) { + this.println(DebuggerPDP10.PROMPT + sCmd); + } + + this.dbgAddrNextCode = this.newAddr(this.cpu.getPC()); + + /* + * this.nStep used to be a simple boolean, but now it's 0 (or undefined) + * if inactive, 1 if stepping over an instruction without a register dump, or 2 + * if stepping over an instruction with a register dump. + */ + if (!fRegs || this.nStep == 1) { + this.doUnassemble(); + } else { + this.doRegisters(); + } + } + + /** + * checkCPU(fQuiet) + * + * Make sure the CPU is ready (finished initializing), powered, not already running, and not in an error state. + * + * @this {DebuggerPDP10} + * @param {boolean} [fQuiet] + * @return {boolean} + */ + checkCPU(fQuiet) + { + if (!this.cpu || !this.cpu.isReady() || !this.cpu.isPowered() || this.cpu.isRunning()) { + if (!fQuiet) this.println("cpu busy or unavailable, command ignored"); + return false; + } + return !this.cpu.isError(); + } + + /** + * powerUp(data, fRepower) + * + * @this {DebuggerPDP10} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (!fRepower) { + /* + * Because Debugger save/restore support is somewhat limited (and didn't always exist), + * we deviate from the typical save/restore design pattern: instead of reset OR restore, + * we always reset and then perform a (potentially limited) restore. + */ + this.reset(true); + + // this.println(data? "resuming" : "powering up"); + + if (data) { + return this.restore(data); + } + } + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * @this {DebuggerPDP10} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} + */ + powerDown(fSave, fShutdown) + { + if (fShutdown) this.println(fSave? "suspending" : "shutting down"); + return fSave? this.save() : true; + } + + /** + * reset(fQuiet) + * + * This is a notification handler, called by the Computer, to inform us of a reset. + * + * @this {DebuggerPDP10} + * @param {boolean} fQuiet (true only when called from our own powerUp handler) + */ + reset(fQuiet) + { + this.historyInit(); + this.cInstructions = this.cInstructionsStart = 0; + this.sMessagePrev = null; + this.nCycles = 0; + this.dbgAddrNextCode = this.newAddr(this.cpu.getPC()); + /* + * fRunning is set by start() and cleared by stop(). In addition, we clear + * it here, so that if the CPU is reset while running, we can prevent stop() + * from unnecessarily dumping the CPU state. + */ + this.flags.running = false; + this.clearTempBreakpoint(); + if (!fQuiet) this.updateStatus(); + } + + /** + * save() + * + * This implements (very rudimentary) save support for the Debugger component. + * + * @this {DebuggerPDP10} + * @return {Object} + */ + save() + { + var state = new State(this); + state.set(0, this.packAddr(this.dbgAddrNextCode)); + state.set(1, this.packAddr(this.dbgAddrAssemble)); + state.set(2, [this.aPrevCmds, this.fAssemble, this.bitsMessage]); + state.set(3, this.aSymbolTable); + return state.data(); + } + + /** + * restore(data) + * + * This implements (very rudimentary) restore support for the Debugger component. + * + * @this {DebuggerPDP10} + * @param {Object} data + * @return {boolean} true if successful, false if failure + */ + restore(data) + { + var i = 0; + if (data[2] !== undefined) { + this.dbgAddrNextCode = this.unpackAddr(data[i++]); + this.dbgAddrAssemble = this.unpackAddr(data[i++]); + this.aPrevCmds = data[i][0]; + if (typeof this.aPrevCmds == "string") this.aPrevCmds = [this.aPrevCmds]; + this.fAssemble = data[i][1]; + this.bitsMessage |= data[i][2]; // keep our current message bits set, and simply "add" any extra bits defined by the saved state + } + if (data[3]) this.aSymbolTable = data[3]; + return true; + } + + /** + * start(ms, nCycles) + * + * This is a notification handler, called by the Computer, to inform us the CPU has started. + * + * @this {DebuggerPDP10} + * @param {number} ms + * @param {number} nCycles + */ + start(ms, nCycles) + { + if (!this.nStep) this.println("running"); + this.flags.running = true; + this.msStart = ms; + this.nCyclesStart = nCycles; + } + + /** + * stop(ms, nCycles) + * + * This is a notification handler, called by the Computer, to inform us the CPU has now stopped. + * + * @this {DebuggerPDP10} + * @param {number} ms + * @param {number} nCycles + */ + stop(ms, nCycles) + { + if (this.flags.running) { + this.flags.running = false; + this.nCycles = nCycles - this.nCyclesStart; + if (!this.nStep) { + var sStopped = "stopped"; + if (this.nCycles) { + var msTotal = ms - this.msStart; + var nCyclesPerSecond = (msTotal > 0? Math.round(this.nCycles * 1000 / msTotal) : 0); + sStopped += " ("; + if (this.checksEnabled()) { + sStopped += this.cInstructions + " instructions, "; + /* + * $ops displays progress by calculating cInstructions - cInstructionsStart, so before + * zeroing cInstructions, we should subtract cInstructions from cInstructionsStart (since + * we're effectively subtracting cInstructions from cInstructions as well). + */ + this.cInstructionsStart -= this.cInstructions; + this.cInstructions = 0; + } + sStopped += this.nCycles + " cycles, " + msTotal + " ms, " + nCyclesPerSecond + " hz)"; + } else { + if (this.messageEnabled(MessagesPDP10.HALT)) { + /* + * It's possible the user is trying to 'g' past a fault that was blocked by helpCheckFault() + * for the Debugger's benefit; if so, it will continue to be blocked, so try displaying a helpful + * message (another helpful tip would be to simply turn off the "halt" message category). + */ + sStopped += " (use the 't' command to execute blocked faults)"; + } + } + this.println(sStopped); + } + this.updateStatus(true); + this.setFocus(); + this.clearTempBreakpoint(this.cpu.getPC()); + this.sMessagePrev = null; + } + } + + /** + * checksEnabled(fRelease) + * + * This "check" function is called by the CPU; we indicate whether or not every instruction needs to be checked. + * + * Originally, this returned true even when there were only read and/or write breakpoints, but those breakpoints + * no longer require the intervention of checkInstruction(); the Bus component automatically swaps in/out appropriate + * "checked" Memory access functions to deal with those breakpoints in the corresponding Memory blocks. So I've + * simplified the test below. + * + * @this {DebuggerPDP10} + * @param {boolean} [fRelease] is true for release criteria only; default is false (any criteria) + * @return {boolean} true if every instruction needs to pass through checkInstruction(), false if not + */ + checksEnabled(fRelease) + { + return ((DEBUG && !fRelease)? true : (this.aBreakExec.length > 1 || !!this.nBreakInstructions)); + } + + /** + * checkInstruction(addr, nState) + * + * This "check" function is called by the CPU to inform us about the next instruction to be executed, + * giving us an opportunity to look for "exec" breakpoints and update opcode instruction history. + * + * @this {DebuggerPDP10} + * @param {number} addr + * @param {number} nState is < 0 if stepping, 0 if starting, or > 0 if running + * @return {boolean} true if breakpoint hit, false if not + */ + checkInstruction(addr, nState) + { + var opCode = -1; + var cpu = this.cpu; + + /* + * If opHalt() calls our stopInstruction() function, it will effectively rewind the PC back to the HALT, + * purely for our debugging benefit, so we must compensate for that here by advancing the PC past the HALT + * when the machine starts up again. + */ + if (!nState) { + opCode = this.cpu.readWord(addr); + if (opCode == PDP10.OPCODE.HALT && this.cpu.getLastPC() == addr) { + addr = this.cpu.advancePC(1); + } + } + + /* + * If the CPU stopped on a breakpoint, we're not interested in stopping again if the machine is starting. + */ + if (nState > 0) { + if (this.nBreakInstructions) { + if (!--this.nBreakInstructions) return true; + } + if (this.checkBreakpoint(addr, 1, this.aBreakExec)) { + return true; + } + } + + /* + * The rest of the instruction tracking logic can only be performed if historyInit() has allocated the + * necessary data structures. Note that there is no explicit UI for enabling/disabling history, other than + * adding/removing breakpoints, simply because it's breakpoints that trigger the call to checkInstruction(); + * well, OK, and a few other things now, like enabling MessagesPDP10.INT messages. + */ + if (nState >= 0 && this.aInstructionHistory.length) { + this.cInstructions++; + if (opCode < 0) { + opCode = this.cpu.readWord(addr); + } + if ((opCode & 0xffff) != PDP10.OPCODE.INVALID) { + var dbgAddr = this.aInstructionHistory[this.iInstructionHistory]; + this.setAddr(dbgAddr, addr); + // if (DEBUG) dbgAddr.cycleCount = cpu.getCycles(); + if (++this.iInstructionHistory == this.aInstructionHistory.length) this.iInstructionHistory = 0; + } + } + return false; + } + + /** + * stopInstruction(sMessage) + * + * TODO: Currently, the only way to prevent this call from stopping the CPU is when you're single-stepping. + * + * @this {DebuggerPDP10} + * @param {string} [sMessage] + * @return {boolean} true if stopping is enabled, false if not + */ + stopInstruction(sMessage) + { + var cpu = this.cpu; + if (cpu.isRunning()) { + cpu.setPC(this.cpu.getLastPC()); + if (sMessage) this.println(sMessage); + this.stopCPU(); + /* + * TODO: Review the appropriate-ness of throwing a bogus vector number in order to immediately stop + * the instruction. It's handy, but it also means that we no longer actually return true, so callers + * of either stopInstruction() or undefinedInstruction() may have unreachable code paths. + */ + throw -1; + } + return false; + } + + /** + * undefinedInstruction(opCode) + * + * @this {DebuggerPDP10} + * @param {number} opCode + * @return {boolean} true if stopping is enabled, false if not + */ + undefinedInstruction(opCode) + { + if (this.messageEnabled(MessagesPDP10.CPU)) { + this.printMessage("undefined opcode " + this.toStrBase(opCode), true, true); + return this.stopInstruction(); // allow the caller to step over it if they really want a trap generated + } + return false; + } + + /** + * checkMemoryRead(addr, nb) + * + * This "check" function is called by a Memory block to inform us that a memory read occurred, giving us an + * opportunity to track the read if we want, and look for a matching "read" breakpoint, if any. + * + * In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now + * Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" read. + * + * If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing. + * + * @this {DebuggerPDP10} + * @param {number} addr + * @param {number} [nb] (# of bytes; default is 1) + * @return {boolean} true if breakpoint hit, false if not + */ + checkMemoryRead(addr, nb) + { + if (this.checkBreakpoint(addr, nb || 1, this.aBreakRead)) { + this.stopCPU(false); + return true; + } + return false; + } + + /** + * checkMemoryWrite(addr, nb) + * + * This "check" function is called by a Memory block to inform us that a memory write occurred, giving us an + * opportunity to track the write if we want, and look for a matching "write" breakpoint, if any. + * + * In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now + * Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" write. + * + * If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing. + * + * @this {DebuggerPDP10} + * @param {number} addr + * @param {number} [nb] (# of bytes; default is 1) + * @return {boolean} true if breakpoint hit, false if not + */ + checkMemoryWrite(addr, nb) + { + if (this.checkBreakpoint(addr, nb || 1, this.aBreakWrite)) { + this.stopCPU(false); + return true; + } + return false; + } + + /** + * clearBreakpoints() + * + * @this {DebuggerPDP10} + */ + clearBreakpoints() + { + var i, dbgAddr, addr; + this.aBreakExec = ["bp"]; + if (this.aBreakRead !== undefined) { + for (i = 1; i < this.aBreakRead.length; i++) { + dbgAddr = this.aBreakRead[i]; + addr = this.getAddr(dbgAddr); + this.bus.removeMemBreak(addr, false); + } + } + this.aBreakRead = ["br"]; + if (this.aBreakWrite !== undefined) { + for (i = 1; i < this.aBreakWrite.length; i++) { + dbgAddr = this.aBreakWrite[i]; + addr = this.getAddr(dbgAddr); + this.bus.removeMemBreak(addr, true); + } + } + this.aBreakWrite = ["bw"]; + /* + * nSuppressBreaks ensures we can't get into an infinite loop where a breakpoint lookup + * requires reading memory that triggers more memory reads, which triggers more breakpoint checks. + */ + this.nSuppressBreaks = 0; + this.nBreakInstructions = 0; + } + + /** + * addBreakpoint(aBreak, dbgAddr, fTemporary) + * + * In case you haven't already figured this out, all our breakpoint commands use the address + * to identify a breakpoint, not an incrementally assigned breakpoint index like other debuggers; + * see doBreak() for details. + * + * This has a few implications, one being that you CANNOT set more than one kind of breakpoint + * on a single address. In practice, that's rarely a problem, because you can almost always set + * a different breakpoint on a neighboring address. + * + * Also, there is one exception to the "one address, one breakpoint" rule, and that involves + * temporary breakpoints (ie, one-time execution breakpoints that either a "p" or "g" command + * may create to step over a chunk of code). Those breakpoints automatically clear themselves, + * so there usually isn't any need to refer to them using breakpoint commands. + * + * TODO: Consider supporting the more "traditional" breakpoint index syntax; the current + * address-based syntax was implemented solely for expediency and consistency. At the same time, + * also consider a more WDEB386-like syntax, where "br" is used to set a variety of access-specific + * breakpoints, using modifiers like "r1", "r2", "w1", "w2, etc. + * + * @this {DebuggerPDP10} + * @param {Array} aBreak + * @param {DbgAddrPDP10} dbgAddr + * @param {boolean} [fTemporary] + * @return {boolean} true if breakpoint added, false if already exists + */ + addBreakpoint(aBreak, dbgAddr, fTemporary) + { + var fSuccess = true; + + // this.nSuppressBreaks++; + + /* + * Instead of complaining that a breakpoint already exists (as we used to do), we now + * allow breakpoints to be re-set; this makes it easier to update any commands that may + * be associated with the breakpoint. + * + * The only exception: we DO allow a temporary breakpoint at an address where there may + * already be a breakpoint, so that you can easily step ("p" or "g") over such addresses. + */ + if (!fTemporary) { + this.findBreakpoint(aBreak, dbgAddr, true, false, true); + } + + if (aBreak != this.aBreakExec) { + var addr = this.getAddr(dbgAddr); + if (addr === PDP10.ADDR_INVALID) { + this.println("invalid address: " + this.toStrAddr(dbgAddr)); + fSuccess = false; + } else { + var fWrite = (aBreak == this.aBreakWrite); + this.bus.addMemBreak(addr, fWrite); + } + } + + if (fSuccess) { + aBreak.push(dbgAddr); + if (fTemporary) { + dbgAddr.fTemporary = true; + } + else { + this.printBreakpoint(aBreak, aBreak.length-1, "set"); + this.historyInit(); + } + } + + // this.nSuppressBreaks--; + + return fSuccess; + } + + /** + * findBreakpoint(aBreak, dbgAddr, fRemove, fTemporary, fQuiet) + * + * @this {DebuggerPDP10} + * @param {Array} aBreak + * @param {DbgAddrPDP10} dbgAddr + * @param {boolean} [fRemove] + * @param {boolean} [fTemporary] + * @param {boolean} [fQuiet] + * @return {boolean} true if found, false if not + */ + findBreakpoint(aBreak, dbgAddr, fRemove, fTemporary, fQuiet) + { + var fFound = false; + var addr = this.getAddr(dbgAddr); + for (var i = 1; i < aBreak.length; i++) { + var dbgAddrBreak = aBreak[i]; + if (addr == this.getAddr(dbgAddrBreak)) { + if (!fTemporary || dbgAddrBreak.fTemporary) { + fFound = true; + if (fRemove) { + if (!dbgAddrBreak.fTemporary && !fQuiet) { + this.printBreakpoint(aBreak, i, "cleared"); + } + aBreak.splice(i, 1); + if (aBreak != this.aBreakExec) { + var fWrite = (aBreak == this.aBreakWrite); + this.bus.removeMemBreak(addr, fWrite); + } + /* + * We'll mirror the logic in addBreakpoint() and leave the history buffer alone if this + * was a temporary breakpoint. + */ + if (!dbgAddrBreak.fTemporary) { + this.historyInit(); + } + break; + } + if (!fQuiet) this.printBreakpoint(aBreak, i, "exists"); + break; + } + } + } + return fFound; + } + + /** + * listBreakpoints(aBreak) + * + * @this {DebuggerPDP10} + * @param {Array} aBreak + * @return {number} of breakpoints listed, 0 if none + */ + listBreakpoints(aBreak) + { + for (var i = 1; i < aBreak.length; i++) { + this.printBreakpoint(aBreak, i); + } + return aBreak.length - 1; + } + + /** + * printBreakpoint(aBreak, i, sAction) + * + * @this {DebuggerPDP10} + * @param {Array} aBreak + * @param {number} i + * @param {string} [sAction] + */ + printBreakpoint(aBreak, i, sAction) + { + var dbgAddr = aBreak[i]; + this.println(aBreak[0] + ' ' + this.toStrAddr(dbgAddr) + (sAction? (' ' + sAction) : (dbgAddr.sCmd? (' "' + dbgAddr.sCmd + '"') : ''))); + } + + /** + * setTempBreakpoint(dbgAddr) + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10} dbgAddr of new temp breakpoint + */ + setTempBreakpoint(dbgAddr) + { + this.addBreakpoint(this.aBreakExec, dbgAddr, true); + } + + /** + * clearTempBreakpoint(addr) + * + * @this {DebuggerPDP10} + * @param {number|undefined} [addr] clear all temp breakpoints if no address specified + */ + clearTempBreakpoint(addr) + { + if (addr !== undefined) { + this.checkBreakpoint(addr, 1, this.aBreakExec, true); + this.nStep = 0; + } else { + for (var i = 1; i < this.aBreakExec.length; i++) { + var dbgAddrBreak = this.aBreakExec[i]; + if (dbgAddrBreak.fTemporary) { + if (!this.findBreakpoint(this.aBreakExec, dbgAddrBreak, true, true)) break; + i = 0; + } + } + } + } + + /** + * checkBreakpoint(addr, nb, aBreak, fTemporary) + * + * @this {DebuggerPDP10} + * @param {number} addr + * @param {number} nb (# of bytes) + * @param {Array} aBreak + * @param {boolean} [fTemporary] + * @return {boolean} true if breakpoint has been hit, false if not + */ + checkBreakpoint(addr, nb, aBreak, fTemporary) + { + /* + * Time to check for breakpoints; note that this should be done BEFORE updating history data + * (see checkInstruction), since we might not actually execute the current instruction. + */ + var fBreak = false; + + if (!this.nSuppressBreaks++) { + + for (var i = 1; !fBreak && i < aBreak.length; i++) { + + var dbgAddrBreak = aBreak[i]; + + if (fTemporary && !dbgAddrBreak.fTemporary) continue; + + /* + * If we're checking an execution address, which is always virtual, and virtual + * addresses are always restricted to 16 bits, let's mask the breakpoint address to match + * (the user should know better, but we'll be nice). + */ + var addrBreak = this.getAddr(dbgAddrBreak) & (aBreak == this.aBreakExec? 0xffff : -1); + for (var n = 0; n < nb; n++) { + + if ((addr + n) != addrBreak) continue; + + var a; + fBreak = true; + if (dbgAddrBreak.fTemporary) { + this.findBreakpoint(aBreak, dbgAddrBreak, true, true); + fTemporary = true; + } + if (a = dbgAddrBreak.aCmds) { + /* + * When one or more commands are attached to a breakpoint, we don't halt by default. + * Instead, we set fBreak to true only if, at the completion of all the commands, the + * CPU is halted; in other words, you should include "h" as one of the breakpoint commands + * if you want the breakpoint to stop execution. + * + * Another useful command is "if", which will return false if the expression is false, + * at which point we'll jump ahead to the next "else" command, and if there isn't an "else", + * we abort. + */ + fBreak = false; + for (var j = 0; j < a.length; j++) { + if (!this.doCommand(a[j], true)) { + if (a[j].indexOf("if")) { + fBreak = true; // the failed command wasn't "if", so abort + break; + } + var k = j + 1; + for (; k < a.length; k++) { + if (!a[k].indexOf("else")) break; + j++; + } + if (k == a.length) { // couldn't find an "else" after the "if", so abort + fBreak = true; + break; + } + /* + * If we're still here, we'll execute the "else" command (which is just a no-op), + * followed by any remaining commands. + */ + } + } + if (!this.cpu.isRunning()) fBreak = true; + } + if (fBreak) { + if (!fTemporary) this.printBreakpoint(aBreak, i, "hit"); + break; + } + } + } + } + + this.nSuppressBreaks--; + + return fBreak; + } + + /** + * getInstruction(dbgAddr, sComment, nSequence) + * + * Get the next instruction, by decoding the opcode and any operands. + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10} dbgAddr + * @param {string} [sComment] is an associated comment + * @param {number|null} [nSequence] is an associated sequence number, undefined if none + * @return {string} (and dbgAddr is updated to the next instruction) + */ + getInstruction(dbgAddr, sComment, nSequence) + { + var opNames = DebuggerPDP10.OPNAMES; + var dbgAddrOp = this.newAddr(dbgAddr.addr); + var opCode = this.getWord(dbgAddr, 2); + + var opDesc; + for (var mask in this.opTable) { + var opMasks = this.opTable[mask]; + opDesc = opMasks[opCode & mask]; + if (opDesc) break; + } + + if (!opDesc) { + opDesc = DebuggerPDP10.OPNONE; + } + + var opNum = opDesc[0]; + if (this.aOpReserved.indexOf(opNum) >= 0) { + opDesc = DebuggerPDP10.OPNONE; + opNum = opDesc[0]; + } + + var sOperands = "", sTarget = ""; + var sOpName = opNames[opNum]; + var cOperands = opDesc.length - 1; + + if (!opNum && !cOperands) { + sOperands = this.toStrBase(opCode); + } + + for (var iOperand = 1; iOperand <= cOperands; iOperand++) { + + var opType = opDesc[iOperand]; + if (opType === undefined) continue; + + var sOperand = this.getOperand(opCode, opType, dbgAddr); + + if (!sOperand || !sOperand.length) { + sOperands = "INVALID"; + break; + } + + /* + * If getOperand() returns an Array rather than a string, then the first element is the original + * operand, and the second element contains additional information (eg, the target) of the operand. + */ + if (typeof sOperand != "string") { + sTarget = sOperand[1]; + sOperand = sOperand[0]; + } + + if (sOperands.length > 0) sOperands += ','; + sOperands += (sOperand || "???"); + } + + var sOpCodes = ""; + var sLine = this.toStrAddr(dbgAddrOp) + ":"; + if (dbgAddrOp.addr !== PDP10.ADDR_INVALID && dbgAddr.addr !== PDP10.ADDR_INVALID) { + do { + sOpCodes += ' ' + this.toStrBase(this.getWord(dbgAddrOp, 2)); + if (dbgAddrOp.addr == null) break; + } while (dbgAddrOp.addr != dbgAddr.addr); + } + + sLine += Str.pad(sOpCodes, 24); + sLine += Str.pad(sOpName, 5); + if (sOperands) sLine += ' ' + sOperands; + + if (sComment || sTarget) { + sLine = Str.pad(sLine, 60) + ';' + (sComment || ""); + if (!this.cpu.flags.checksum) { + sLine += (nSequence != null? '=' + nSequence.toString() : ""); + } else { + var nCycles = this.cpu.getCycles(); + sLine += "cycles=" + nCycles.toString() + " cs=" + Str.toHex(this.cpu.nChecksum); + } + if (sTarget) { + if (sLine.slice(-1) != ';') sLine += ' '; + sLine += sTarget; + } + } + return sLine; + } + + /** + * getOperand(opCode, opType, dbgAddr) + * + * If getOperand() returns an Array rather than a string, then the first element is the original + * operand, and the second element is a comment containing additional information (eg, the target) + * of the operand. + * + * @this {DebuggerPDP10} + * @param {number} opCode + * @param {number} opType + * @param {DbgAddrPDP10} dbgAddr + * @return {string|Array.} + */ + getOperand(opCode, opType, dbgAddr) + { + return ""; + } + + /** + * parseInstruction(sOp, sOperand, addr) + * + * TODO: Unimplemented. See parseInstruction() in modules/c1pjs/lib/debugger.js for a sample implementation. + * + * @this {DebuggerPDP10} + * @param {string} sOp + * @param {string|undefined} sOperand + * @param {DbgAddrPDP10} dbgAddr of memory where this instruction is being assembled + * @return {Array.} of opcode bytes; if the instruction can't be parsed, the array will be empty + */ + parseInstruction(sOp, sOperand, dbgAddr) + { + var aOpBytes = []; + this.println("not supported yet"); + return aOpBytes; + } + + /** + * getRegOutput(iReg) + * + * @this {DebuggerPDP10} + * @param {number} iReg + * @return {string} + */ + getRegOutput(iReg) + { + var sReg = this.getRegName(iReg) || "undefined"; + if (sReg) sReg += ' '; + return sReg; + } + + /** + * getMiscDump() + * + * @this {DebuggerPDP10} + * @return {string} + */ + getMiscDump() + { + return ""; + } + + /** + * getRegDump(fMisc) + * + * @this {DebuggerPDP10} + * @param {boolean} [fMisc] (true to include misc registers) + * @return {string} + */ + getRegDump(fMisc) + { + var i; + var sDump = ""; + sDump += this.getRegOutput(DebuggerPDP10.REGS.PC); + if (fMisc) sDump += '\n' + this.getMiscDump(); + return sDump; + } + + /** + * comparePairs(p1, p2) + * + * @this {DebuggerPDP10} + * @param {number|string|Array|Object} p1 + * @param {number|string|Array|Object} p2 + * @return {number} + */ + comparePairs(p1, p2) + { + return p1[0] > p2[0]? 1 : p1[0] < p2[0]? -1 : 0; + } + + /** + * addSymbols(sModule, addr, len, aSymbols) + * + * As filedump.js (formerly convrom.php) explains, aSymbols is a JSON-encoded object whose properties consist + * of all the symbols (in upper-case), and the values of those properties are objects containing any or all of + * the following properties: + * + * 'v': the value of an absolute (unsized) value + * 'b': either 1, 2, 4 or undefined if an unsized value + * 's': either a hard-coded segment or undefined + * 'o': the offset of the symbol within the associated address space + * 'l': the original-case version of the symbol, present only if it wasn't originally upper-case + * 'a': annotation for the specified offset; eg, the original assembly language, with optional comment + * + * To that list of properties, we also add: + * + * 'p': the physical address (calculated whenever both 's' and 'o' properties are defined) + * + * Note that values for any 'v', 'b', 's' and 'o' properties are unquoted decimal values, and the values + * for any 'l' or 'a' properties are quoted strings. Also, if double-quotes were used in any of the original + * annotation ('a') values, they will have been converted to two single-quotes, so we're responsible for + * converting them back to individual double-quotes. + * + * For example: + * { + * 'HF_PORT': { + * 'v':800 + * }, + * 'HDISK_INT': { + * 'b':4, 's':0, 'o':52 + * }, + * 'ORG_VECTOR': { + * 'b':4, 's':0, 'o':76 + * }, + * 'CMD_BLOCK': { + * 'b':1, 's':64, 'o':66 + * }, + * 'DISK_SETUP': { + * 'o':3 + * }, + * '.40': { + * 'o':40, 'a':"MOV AX,WORD PTR ORG_VECTOR ;GET DISKETTE VECTOR" + * } + * } + * + * If a symbol only has an offset, then that offset value can be assigned to the symbol property directly: + * + * 'DISK_SETUP': 3 + * + * The last property is an example of an "anonymous" entry, for offsets where there is no associated symbol. + * Such entries are identified by a period followed by a unique number (usually the offset of the entry), and + * they usually only contain offset ('o') and annotation ('a') properties. I could eliminate the leading + * period, but it offers a very convenient way of quickly discriminating among genuine vs. anonymous symbols. + * + * We add all these entries to our internal symbol table, which is an array of 4-element arrays, each of which + * look like: + * + * [addr, len, aSymbols, aOffsets] + * + * There are two basic symbol operations: findSymbol(), which takes an address and finds the symbol, if any, + * at that address, and findSymbolAddr(), which takes a string and attempts to match it to a non-anonymous + * symbol with a matching offset ('o') property. + * + * To implement findSymbol() efficiently, addSymbols() creates an array of [offset, sSymbol] pairs + * (aOffsets), one pair for each symbol that corresponds to an offset within the specified address space. + * + * We guarantee the elements of aOffsets are in offset order, because we build it using binaryInsert(); + * it's quite likely that the MAP file already ordered all its symbols in offset order, but since they're + * hand-edited files, we can't assume that, and we need to ensure that findSymbol()'s binarySearch() operates + * properly. + * + * @this {DebuggerPDP10} + * @param {string|null} sModule + * @param {number|null} addr (physical address where the symbols are located, if the memory is physical; eg, ROM) + * @param {number} len (the size of the region, in bytes) + * @param {Object} aSymbols (collection of symbols in this group; the format of this collection is described below) + */ + addSymbols(sModule, addr, len, aSymbols) + { + var dbgAddr = {}; + var aOffsets = []; + for (var sSymbol in aSymbols) { + var symbol = aSymbols[sSymbol]; + if (typeof symbol == "number") { + aSymbols[sSymbol] = symbol = {'o': symbol}; + } + var offSymbol = symbol['o']; + var sAnnotation = symbol['a']; + if (offSymbol !== undefined) { + Usr.binaryInsert(aOffsets, [offSymbol >>> 0, sSymbol], this.comparePairs); + } + if (sAnnotation) symbol['a'] = sAnnotation.replace(/''/g, "\""); + } + var symbolTable = { + sModule: sModule, + addr: addr, + len: len, + aSymbols: aSymbols, + aOffsets: aOffsets + }; + this.aSymbolTable.push(symbolTable); + } + + /** + * dumpSymbols() + * + * TODO: Add "numerical" and "alphabetical" dump options. This is simply dumping them in whatever + * order they appeared in the original MAP file. + * + * @this {DebuggerPDP10} + */ + dumpSymbols() + { + for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) { + var symbolTable = this.aSymbolTable[iTable]; + for (var sSymbol in symbolTable.aSymbols) { + if (sSymbol.charAt(0) == '.') continue; + var symbol = symbolTable.aSymbols[sSymbol]; + var offSymbol = symbol['o']; + if (offSymbol === undefined) continue; + var sSymbolOrig = symbolTable.aSymbols[sSymbol]['l']; + if (sSymbolOrig) sSymbol = sSymbolOrig; + this.println(this.toStrOffset(offSymbol) + ' ' + sSymbol); + } + } + } + + /** + * findSymbol(dbgAddr, fNearest) + * + * Search aSymbolTable for dbgAddr, and return an Array for the corresponding symbol (empty if not found). + * + * If fNearest is true, and no exact match was found, then the Array returned will contain TWO sets of + * entries: [0]-[3] will refer to closest preceding symbol, and [4]-[7] will refer to the closest subsequent symbol. + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10} dbgAddr + * @param {boolean} [fNearest] + * @return {Array} where [0] == symbol name, [1] == symbol value, [2] == any annotation, and [3] == any associated comment + */ + findSymbol(dbgAddr, fNearest) + { + var aSymbol = []; + var addrSymbol = this.getAddr(dbgAddr) >>> 0; + for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) { + var symbolTable = this.aSymbolTable[iTable]; + var addr = symbolTable.addr >>> 0; + var len = symbolTable.len; + if (addrSymbol >= addr && addrSymbol < addr + len) { + var offSymbol = addrSymbol - addr; + var result = Usr.binarySearch(symbolTable.aOffsets, [offSymbol], this.comparePairs); + if (result >= 0) { + this.returnSymbol(iTable, result, aSymbol); + } + else if (fNearest) { + result = ~result; + this.returnSymbol(iTable, result-1, aSymbol); + this.returnSymbol(iTable, result, aSymbol); + } + break; + } + } + return aSymbol; + } + + /** + * findSymbolAddr(sSymbol) + * + * Search our symbol tables for sSymbol, and if found, return a dbgAddr (same as parseAddr()). + * + * @this {DebuggerPDP10} + * @param {string} sSymbol + * @return {DbgAddrPDP10|undefined} + */ + findSymbolAddr(sSymbol) + { + var dbgAddr, offSymbol; + + if (sSymbol.match(/^[a-z_][a-z0-9_]*$/i)) { + var sUpperCase = sSymbol.toUpperCase(); + for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) { + var symbolTable = this.aSymbolTable[iTable]; + var symbol = symbolTable.aSymbols[sUpperCase]; + if (symbol != null) { + offSymbol = symbol['o']; + /* + * If the symbol matched but there's no 'o' offset (ie, it wasn't for an address), there's + * no point looking any farther, since each symbol appears only once. + * + * NOTE: We assume that every ROM is ORG'ed at 0x0000, and therefore unless the symbol has an + * explicitly-defined segment, we return the segment associated with the entire group; for a ROM, + * that segment is normally "addrROM >>> 4". Down the road, we may want/need to support a special + * symbol entry (eg, ".ORG") that defines an alternate origin. + */ + break; + } + } + } + if (offSymbol != null) { + dbgAddr = this.newAddr(offSymbol); + } + return dbgAddr; + } + + /** + * returnSymbol(iTable, iOffset, aSymbol) + * + * Helper function for findSymbol(). + * + * @param {number} iTable + * @param {number} iOffset + * @param {Array} aSymbol is updated with the specified symbol, if it exists + */ + returnSymbol(iTable, iOffset, aSymbol) + { + var symbol = {}; + var aOffsets = this.aSymbolTable[iTable].aOffsets; + var offset = 0, sSymbol = null; + if (iOffset >= 0 && iOffset < aOffsets.length) { + offset = aOffsets[iOffset][0]; + sSymbol = aOffsets[iOffset][1]; + } + if (sSymbol) { + symbol = this.aSymbolTable[iTable].aSymbols[sSymbol]; + sSymbol = (sSymbol.charAt(0) == '.'? null : (symbol['l'] || sSymbol)); + } + aSymbol.push(sSymbol); + aSymbol.push(offset); + aSymbol.push(symbol['a']); + aSymbol.push(symbol['c']); + } + + /** + * doHelp() + * + * @this {DebuggerPDP10} + */ + doHelp() + { + var s = "commands:"; + for (var sCommand in DebuggerPDP10.COMMANDS) { + s += '\n' + Str.pad(sCommand, 9) + DebuggerPDP10.COMMANDS[sCommand]; + } + if (!this.checksEnabled()) s += "\nnote: history disabled if no exec breakpoints"; + this.println(s); + } + + /** + * doAssemble(asArgs) + * + * This always receives the complete argument array, where the order of the arguments is: + * + * [0]: the assemble command (assumed to be "a") + * [1]: the target address (eg, "200") + * [2]: the operation code, aka instruction name (eg, "adc") + * [3]: the operation mode operand, if any (eg, "14", "[1234]", etc) + * + * The Debugger enters "assemble mode" whenever only the first (or first and second) arguments are present. + * As long as "assemble mode is active, the user can omit the first two arguments on all later assemble commands + * until "assemble mode" is cancelled with an empty command line; the command processor automatically prepends "a" + * and the next available target address to the argument array. + * + * Entering "assemble mode" is optional; one could enter a series of fully-qualified assemble commands; eg: + * + * a ff00 cld + * a ff01 ldx 28 + * ... + * + * without ever entering "assemble mode", but of course, that requires more typing and doesn't take advantage + * of automatic target address advancement (see dbgAddrAssemble). + * + * NOTE: As the previous example implies, you can even assemble new instructions into ROM address space; + * as our setByte() function explains, the ROM write-notification handlers only refuse writes from the CPU. + * + * @this {DebuggerPDP10} + * @param {Array.} asArgs is the complete argument array, beginning with the "a" command in asArgs[0] + */ + doAssemble(asArgs) + { + var dbgAddr = this.parseAddr(asArgs[1], true); + if (!dbgAddr) return; + + this.dbgAddrAssemble = dbgAddr; + if (asArgs[2] === undefined) { + this.println("begin assemble at " + this.toStrAddr(dbgAddr)); + this.fAssemble = true; + this.cmp.updateDisplays(); + return; + } + + var aOps = this.parseInstruction(asArgs[2], asArgs[3], dbgAddr); + if (aOps.length) { + for (var i = 0; i < aOps.length; i++) { + this.setWord(dbgAddr, aOps[i], 1); + } + /* + * Since getInstruction() also updates the specified address, dbgAddrAssemble is automatically advanced. + */ + this.println(this.getInstruction(this.dbgAddrAssemble)); + } + } + + /** + * doBreak(sCmd, sAddr, sOptions) + * + * As the "help" output below indicates, the following breakpoint commands are supported: + * + * bp # set exec breakpoint + * br # set read breakpoint + * bw # set write breakpoint + * bc # clear breakpoint (* to clear all) + * bl list all breakpoints + * bn [#] break after # instruction(s) + * + * The "bn" command, like the "dh" command and all other commands that use an instruction count, + * assumes a decimal value, regardless of the current base. Use "bn" without an argument to display + * the break count, and use "bn 0" to clear the break count. + * + * @this {DebuggerPDP10} + * @param {string} sCmd + * @param {string|undefined} [sAddr] + * @param {string} [sOptions] (the rest of the breakpoint command-line) + */ + doBreak(sCmd, sAddr, sOptions) + { + if (sAddr == '?') { + this.println("breakpoint commands:"); + this.println("\tbp #\tset exec breakpoint"); + this.println("\tbr #\tset read breakpoint"); + this.println("\tbw #\tset write breakpoint"); + this.println("\tbc #\tclear breakpoint (* to clear all)"); + this.println("\tbl\tlist all breakpoints"); + this.println("\tbn [#]\tbreak after # instruction(s)"); + return; + } + + var sParm = sCmd.charAt(1); + if (sParm == 'l') { + var cBreaks = 0; + cBreaks += this.listBreakpoints(this.aBreakExec); + cBreaks += this.listBreakpoints(this.aBreakRead); + cBreaks += this.listBreakpoints(this.aBreakWrite); + if (!cBreaks) this.println("no breakpoints"); + return; + } + + if (sParm == 'n') { + var n = +sAddr || 0; + if (sAddr) this.nBreakInstructions = n; + this.println("break after " + n + " instruction(s)"); + return; + } + + if (sAddr === undefined) { + this.println("missing breakpoint address"); + return; + } + + var dbgAddr = this.newAddr(); + if (sAddr != '*') { + dbgAddr = this.parseAddr(sAddr, true, true); + if (!dbgAddr) return; + } + + if (sParm == 'c') { + if (dbgAddr.addr == null) { + this.clearBreakpoints(); + this.println("all breakpoints cleared"); + return; + } + if (this.findBreakpoint(this.aBreakExec, dbgAddr, true)) + return; + if (this.findBreakpoint(this.aBreakRead, dbgAddr, true)) + return; + if (this.findBreakpoint(this.aBreakWrite, dbgAddr, true)) + return; + this.println("breakpoint missing: " + this.toStrAddr(dbgAddr)); + return; + } + + if (dbgAddr.addr == null) return; + + this.parseAddrOptions(dbgAddr, sOptions); + + if (sParm == 'p') { + this.addBreakpoint(this.aBreakExec, dbgAddr); + return; + } + if (sParm == 'r') { + this.addBreakpoint(this.aBreakRead, dbgAddr); + return; + } + if (sParm == 'w') { + this.addBreakpoint(this.aBreakWrite, dbgAddr); + return; + } + this.println("unknown breakpoint command: " + sParm); + } + + /** + * doClear(sCmd) + * + * @this {DebuggerPDP10} + * @param {string} [sCmd] (eg, "cls" or "clear") + */ + doClear(sCmd) + { + /* + * TODO: There should be a clear() component method that the Control Panel overrides to perform this function. + */ + if (this.controlPrint) this.controlPrint.value = ""; + } + + /** + * doDump(asArgs) + * + * @this {DebuggerPDP10} + * @param {Array.} asArgs (formerly sCmd, [sAddr], [sLen] and [sBytes]) + */ + doDump(asArgs) + { + var m; + var sCmd = asArgs[0]; + var sAddr = asArgs[1]; + var sLen = asArgs[2]; + var sBytes = asArgs[3]; + + if (sAddr == '?') { + var sDumpers = ""; + for (m in MessagesPDP10.CATEGORIES) { + if (this.afnDumpers[m]) { + if (sDumpers) sDumpers += ','; + sDumpers = sDumpers + m; + } + } + sDumpers += ",state,symbols"; + this.println("dump memory commands:"); + this.println("\tdw [a] [n] dump n words at address a"); + this.println("\tds [a] [n] dump n words at address a as JSON"); + this.println("\tdh [p] [n] dump n instructions from history position p"); + if (sDumpers.length) this.println("dump extension commands:\n\t" + sDumpers); + return; + } + + if (sAddr == "state") { + var sState = this.cmp.powerOff(true); + if (sLen == "console") { + /* + * Console buffers are notoriously small, and even the following code, which breaks the + * data into parts (eg, "d state console 1", "d state console 2", etc) just isn't that helpful. + * + * var nPart = +sBytes; + * if (nPart) sState = sState.substr(1000000 * (nPart-1), 1000000); + * + * So, the best way to capture a large machine state is to use the new "Save Machine" link + * that downloads a machine's entire state. Alternatively, run your own local server and use + * server-side storage. Take a look at the "Save" binding in computer.js, which binds an HTML + * control to the computer.powerOff() and computer.saveServerState() functions. + */ + console.log(sState); + } else { + this.doClear(); + if (sState) this.println(sState); + } + return; + } + + if (sAddr == "symbols") { + this.dumpSymbols(); + return; + } + + if (sCmd == "d") { + for (m in MessagesPDP10.CATEGORIES) { + if (asArgs[1] == m) { + var fnDumper = this.afnDumpers[m]; + if (fnDumper) { + asArgs.shift(); + asArgs.shift(); + fnDumper(asArgs); + } else { + this.println("no dump registered for " + sAddr); + } + return; + } + } + if (!sAddr) sCmd = this.sCmdDumpPrev || "dw"; + } else { + this.sCmdDumpPrev = sCmd; + } + + if (sCmd == "dh") { + this.dumpHistory(sAddr, sLen); + return; + } + + var dbgAddr = this.parseAddr(sAddr); + if (!dbgAddr) return; + + var len = 0; + var fJSON = (sCmd == "ds"); + + if (sLen) { + if (sLen.charAt(0) == 'l') { + sLen = sLen.substr(1) || sBytes; + len = this.parseValue(sLen); + } + else { + var dbgAddrEnd = this.parseAddr(sLen); + if (dbgAddrEnd) len = dbgAddrEnd.addr - dbgAddr.addr; + } + if (len < 0) len = 0; + if (len > 0x10000) len = 0x10000; + } + + var nBase = this.nBase; + if (dbgAddr.nBase) this.nBase = dbgAddr.nBase; + + var nBitsPerWord = 36; + var nWords = len || 32; + var nWordsPerLine = 4; // fJSON? 16 : this.nBase; + var nLines = (((nWords + nWordsPerLine - 1) / nWordsPerLine)|0) || 1; + + var sDump = ""; + while (nLines-- && nWords > 0) { + var sData = "", sChars = ""; + sAddr = this.toStrAddr(dbgAddr); + var n = nWordsPerLine; + while (n-- > 0 && nWords-- > 0) { + var w = this.getWord(dbgAddr, 1); + if (fJSON) { + if (sData) sData += ","; + sData += "0x"+ Str.toHex(w, nBitsPerWord >> 2); + } else { + sData += this.toStrBase(w, nBitsPerWord >> 3); + sData += ' '; + } + var nBytesPerWord = nBitsPerWord >> 3; + while (nBytesPerWord--) { + var c = w % 256; + sChars += (c >= 32 && c < 128? String.fromCharCode(c) : '.'); + w /= 256; + } + } + if (sDump) sDump += "\n"; + if (fJSON) { + sDump += sData + ","; + } else { + sDump += sAddr + ": " + sData + ((n == 0)? (' ' + sChars) : ""); + } + } + + if (sDump) this.println(sDump); + + this.dbgAddrNextData = dbgAddr; + this.nBase = nBase; + } + + /** + * doEdit(asArgs) + * + * @this {DebuggerPDP10} + * @param {Array.} asArgs + */ + doEdit(asArgs) + { + var size, mask; + var fnGet, fnSet; + var sCmd = asArgs[0]; + var sAddr = asArgs[1]; + if (sCmd == "e" || sCmd == "ew") { + size = 2; + mask = 0xffff; + fnGet = this.getWord; + fnSet = this.setWord; + } else { + sAddr = null; + } + if (sAddr == null) { + this.println("edit memory commands:"); + this.println("\tew [a] [...] edit words at address a"); + return; + } + var dbgAddr = this.parseAddr(sAddr); + if (!dbgAddr) return; + for (var i = 2; i < asArgs.length; i++) { + var vNew = this.parseExpression(asArgs[i]); + if (vNew === undefined) { + this.println("unrecognized value: " + asArgs[i]); + break; + } + if (vNew & ~mask) { + this.println("warning: " + Str.toHex(vNew) + " exceeds " + size + "-byte value"); + } + this.println("changing " + this.toStrAddr(dbgAddr) + (this.messageEnabled(MessagesPDP10.BUS)? "" : (" from " + this.toStrBase(fnGet.call(this, dbgAddr), size))) + " to " + this.toStrBase(vNew, size)); + //noinspection JSUnresolvedFunction + fnSet.call(this, dbgAddr, vNew, size); + } + } + + /** + * doHalt(fQuiet) + * + * @this {DebuggerPDP10} + * @param {boolean} [fQuiet] + */ + doHalt(fQuiet) + { + var sMsg; + if (this.flags.running) { + if (!fQuiet) this.println("halting"); + this.stopCPU(); + } else { + if (this.isBusy(true)) return; + if (!fQuiet) this.println("already halted"); + } + } + + /** + * doIf(sCmd, fQuiet) + * + * NOTE: Don't forget that the default base for all numeric constants is 16 (hex), so when you evaluate + * an expression like "a==10", it will compare the value of the variable "a" to 0x10; use a trailing period + * (eg, "10.") if you really intend decimal. + * + * Also, if no variable named "a" exists, "a" will evaluate to 0x0A, so the expression "a==10" becomes + * "0x0A==0x10" (false), whereas the expression "a==10." becomes "0x0A==0x0A" (true). + * + * @this {DebuggerPDP10} + * @param {string} sCmd + * @param {boolean} [fQuiet] + * @return {boolean} true if expression is non-zero, false if zero (or undefined due to a parse error) + */ + doIf(sCmd, fQuiet) + { + sCmd = Str.trim(sCmd); + if (!this.parseExpression(sCmd)) { + if (!fQuiet) this.println("false: " + sCmd); + return false; + } + if (!fQuiet) this.println("true: " + sCmd); + return true; + } + + /** + * doInfo(asArgs) + * + * @this {DebuggerPDP10} + * @param {Array.} asArgs + * @return {boolean} true only if the instruction info command ("n") is supported + */ + doInfo(asArgs) + { + if (DEBUG) { + this.println("msPerYield: " + this.cpu.msPerYield); + this.println("nCyclesPerYield: " + this.cpu.nCyclesPerYield); + return true; + } + return false; + } + + /** + * doVar(sCmd) + * + * The command must be of the form "{variable} = [{expression}]", where expression may contain constants, + * operators, registers, symbols, other variables, or nothing at all; in the latter case, the variable, if + * any, is deleted. + * + * Other supported shorthand: "var" with no parameters prints the values of all variables, and "var {variable}" + * prints the value of the specified variable. + * + * @this {DebuggerPDP10} + * @param {string} sCmd + * @return {boolean} true if valid "var" assignment, false if not + */ + doVar(sCmd) + { + var a = sCmd.match(/^\s*([A-Z_]?[A-Z0-9_]*)\s*(=?)\s*(.*)$/i); + if (a) { + if (!a[1]) { + if (!this.printVariable()) this.println("no variables"); + return true; // it's not considered an error to print an empty list of variables + } + if (!a[2]) { + return this.printVariable(a[1]); + } + if (!a[3]) { + this.delVariable(a[1]); + return true; // it's not considered an error to delete a variable that didn't exist + } + var v = this.parseExpression(a[3]); + if (v !== undefined) { + this.setVariable(a[1], v); + return true; + } + return false; + } + this.println("invalid assignment:" + sCmd); + return false; + } + + /** + * doList(sAddr, fPrint) + * + * @this {DebuggerPDP10} + * @param {string} sAddr + * @param {boolean} [fPrint] + * @return {string|null} + */ + doList(sAddr, fPrint) + { + var sSymbol = null; + + var dbgAddr = this.parseAddr(sAddr, true); + if (dbgAddr) { + var addr = this.getAddr(dbgAddr); + var aSymbol = this.findSymbol(dbgAddr, true); + if (aSymbol.length) { + var nDelta, sDelta, s; + if (aSymbol[0]) { + sDelta = ""; + nDelta = dbgAddr.addr - aSymbol[1]; + if (nDelta) sDelta = " + " + Str.toHexWord(nDelta); + s = aSymbol[0] + " (" + this.toStrOffset(aSymbol[1]) + ')' + sDelta; + if (fPrint) this.println(s); + sSymbol = s; + } + if (aSymbol.length > 4 && aSymbol[4]) { + sDelta = ""; + nDelta = aSymbol[5] - dbgAddr.addr; + if (nDelta) sDelta = " - " + Str.toHexWord(nDelta); + s = aSymbol[4] + " (" + this.toStrOffset(aSymbol[5]) + ')' + sDelta; + if (fPrint) this.println(s); + if (!sSymbol) sSymbol = s; + } + } else { + if (fPrint) this.println("no symbols"); + } + } + return sSymbol; + } + + /** + * doMessages(asArgs) + * + * @this {DebuggerPDP10} + * @param {Array.} asArgs + */ + doMessages(asArgs) + { + var m; + var fCriteria = null; + var sCategory = asArgs[1]; + if (sCategory == '?') sCategory = undefined; + + if (sCategory !== undefined) { + var bitsMessage = 0; + if (sCategory == "all") { + bitsMessage = (0xffffffff|0) & ~(MessagesPDP10.HALT | MessagesPDP10.KEYS | MessagesPDP10.LOG); + sCategory = null; + } else if (sCategory == "on") { + fCriteria = true; + sCategory = null; + } else if (sCategory == "off") { + fCriteria = false; + sCategory = null; + } else { + /* + * Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects, + * but externally, we allow the user to specify "keys"; "kbd" is also allowed as shorthand for "keyboard". + */ + if (sCategory == "keys") sCategory = "key"; + if (sCategory == "kbd") sCategory = "keyboard"; + for (m in MessagesPDP10.CATEGORIES) { + if (sCategory == m) { + bitsMessage = MessagesPDP10.CATEGORIES[m]; + fCriteria = !!(this.bitsMessage & bitsMessage); + break; + } + } + if (!bitsMessage) { + this.println("unknown message category: " + sCategory); + return; + } + } + if (bitsMessage) { + if (asArgs[2] == "on") { + this.bitsMessage |= bitsMessage; + fCriteria = true; + } + else if (asArgs[2] == "off") { + this.bitsMessage &= ~bitsMessage; + fCriteria = false; + if (bitsMessage == MessagesPDP10.BUFFER) { + var i = this.aMessageBuffer.length >= 1000? this.aMessageBuffer.length - 1000 : 0; + while (i < this.aMessageBuffer.length) { + this.println(this.aMessageBuffer[i++]); + } + this.aMessageBuffer = []; + } + } + } + } + + /* + * Display those message categories that match the current criteria (on or off) + */ + var n = 0; + var sCategories = ""; + for (m in MessagesPDP10.CATEGORIES) { + if (!sCategory || sCategory == m) { + var bitMessage = MessagesPDP10.CATEGORIES[m]; + var fEnabled = !!(this.bitsMessage & bitMessage); + if (fCriteria !== null && fCriteria != fEnabled) continue; + if (sCategories) sCategories += ','; + if (!(++n % 10)) sCategories += "\n\t"; // jshint ignore:line + /* + * Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects, + * but externally, we allow the user to specify "keys". + */ + if (m == "key") m = "keys"; + sCategories += m; + } + } + + if (sCategory === undefined) { + this.println("message commands:\n\tm [category] [on|off]\tturn categories on/off"); + } + + this.println((fCriteria !== null? (fCriteria? "messages on: " : "messages off: ") : "message categories:\n\t") + (sCategories || "none")); + + this.historyInit(); // call this just in case MessagesPDP10.INT was turned on + } + + /** + * doOptions(asArgs) + * + * @this {DebuggerPDP10} + * @param {Array.} asArgs + */ + doOptions(asArgs) + { + switch (asArgs[1]) { + + case "base": + if (asArgs[2]) { + var nBase = +asArgs[2]; + if (nBase == 8 || nBase == 10 || nBase == 16) { + this.nBase = nBase; + } else { + this.println("invalid base: " + nBase); + break; + } + } + this.println("default base: " + this.nBase); + break; + + case "cs": + var nCycles; + if (asArgs[3] !== undefined) nCycles = +asArgs[3]; // warning: decimal instead of hex conversion + switch (asArgs[2]) { + case "int": + this.cpu.nCyclesChecksumInterval = nCycles; + break; + case "start": + this.cpu.nCyclesChecksumStart = nCycles; + break; + case "stop": + this.cpu.nCyclesChecksumStop = nCycles; + break; + default: + this.println("unknown cs option"); + return; + } + if (nCycles !== undefined) { + this.cpu.resetChecksum(); + } + this.println("checksums " + (this.cpu.flags.checksum? "enabled" : "disabled")); + return; + + case "sp": + if (asArgs[2] !== undefined) { + if (!this.cpu.setSpeed(+asArgs[2])) { + this.println("warning: using 1x multiplier, previous target not reached"); + } + } + this.println("target speed: " + this.cpu.getSpeedTarget() + " (" + this.cpu.getSpeed() + "x)"); + return; + + default: + if (asArgs[1]) { + this.println("unknown option: " + asArgs[1]); + return; + } + /* falls through */ + + case "?": + this.println("debugger options:"); + this.println("\tbase #\t\tset default base to #"); + this.println("\tcs int #\tset checksum cycle interval to #"); + this.println("\tcs start #\tset checksum cycle start count to #"); + this.println("\tcs stop #\tset checksum cycle stop count to #"); + this.println("\tsp #\t\tset speed multiplier to #"); + break; + } + } + + /** + * doRegisters(asArgs, fInstruction) + * + * @this {DebuggerPDP10} + * @param {Array.} [asArgs] + * @param {boolean} [fInstruction] (true to include the current instruction; default is true) + */ + doRegisters(asArgs, fInstruction) + { + if (asArgs && asArgs[1] == '?') { + this.println("register commands:"); + this.println("\tr\tdump registers"); + this.println("\trm\tdump misc registers"); + this.println("\trx [#]\tset flag or register x to [#]"); + return; + } + + var fMisc = false; + var cpu = this.cpu; + if (fInstruction == null) fInstruction = true; + + if (asArgs != null && asArgs.length > 1) { + var sReg = asArgs[1]; + + if (sReg == 'm') { + fMisc = true; + } + else { + var sValue = null; + var i = sReg.indexOf('='); + if (i > 0) { + sValue = sReg.substr(i + 1); + sReg = sReg.substr(0, i); + } + else if (asArgs.length > 2) { + sValue = asArgs[2]; + } + else { + this.println("missing value for " + asArgs[1]); + return; + } + + var w = this.parseExpression(sValue); + if (w === undefined) return; + + var sRegMatch = sReg.toUpperCase(); + switch (sRegMatch) { + case "PC": + cpu.setPC(w); + this.dbgAddrNextCode = this.newAddr(cpu.getPC()); + break; + default: + this.println("unknown register: " + sReg); + return; + } + this.cmp.updateDisplays(); + this.println("updated registers:"); + } + } + + this.println(this.getRegDump(fMisc)); + + if (fInstruction) { + this.dbgAddrNextCode = this.newAddr(cpu.getPC()); + this.doUnassemble(this.toStrAddr(this.dbgAddrNextCode)); + } + } + + /** + * doRun(sCmd, sAddr, sOptions, fQuiet) + * + * @this {DebuggerPDP10} + * @param {string} sCmd + * @param {string|undefined} [sAddr] + * @param {string} [sOptions] (the rest of the breakpoint command-line) + * @param {boolean} [fQuiet] + */ + doRun(sCmd, sAddr, sOptions, fQuiet) + { + if (sCmd == "gt") { + this.fIgnoreNextCheckFault = true; + } + if (sAddr !== undefined) { + var dbgAddr = this.parseAddr(sAddr, true); + if (!dbgAddr) return; + this.parseAddrOptions(dbgAddr, sOptions); + this.setTempBreakpoint(dbgAddr); + } + this.startCPU(true, fQuiet); + } + + /** + * doPrint(sCmd) + * + * NOTE: If the string to print is a quoted string, then we run it through replaceRegs(), so that + * you can take advantage of all the special replacement options used for software interrupt logging. + * + * @this {DebuggerPDP10} + * @param {string} sCmd + */ + doPrint(sCmd) + { + sCmd = Str.trim(sCmd); + var a = sCmd.match(/^(['"])(.*?)\1$/); + if (!a) { + this.parseExpression(sCmd, true); + } else { + if (a[2].length > 1) { + this.println(this.replaceRegs(a[2])); + } else { + this.printValue(null, a[2].charCodeAt(0)); + } + } + } + + /** + * doStep(sCmd, sOption) + * + * @this {DebuggerPDP10} + * @param {string} [sCmd] "p" or "pr" + * @param {string} [sOption] + */ + doStep(sCmd, sOption) + { + if (sOption == '?') { + this.println("step commands:"); + this.println("\tp\tstep over instruction"); + this.println("\tpr\tstep over instruction with register update"); + return; + } + + var fCallStep = true; + var nRegs = (sCmd == "pr"? 1 : 0); + /* + * Set up the value for this.nStep (ie, 1 or 2) depending on whether the user wants + * a subsequent register dump ("pr") or not ("p"). + */ + var nStep = 1 + nRegs; + + if (!this.nStep) { + var dbgAddr = this.newAddr(this.cpu.getPC()); + var opCode = this.getWord(dbgAddr); + + if (this.nStep) { + this.setTempBreakpoint(dbgAddr); + if (!this.startCPU()) { + if (this.cmp) this.cmp.setFocus(); + this.nStep = 0; + } + /* + * A successful run will ultimately call stop(), which will in turn call clearTempBreakpoint(), + * which will clear nStep, so there's your assurance that nStep will be reset. Now we may have + * stopped for reasons unrelated to the temporary breakpoint, but that's OK. + */ + } else { + this.doTrace(nRegs? "tr" : "t"); + } + } else { + this.println("step in progress"); + } + } + + /** + * getCall(dbgAddr) + * + * Given a possible return address (typically from the stack), look for a matching CALL (or INT) that + * immediately precedes that address. + * + * @this {DebuggerPDP10} + * @param {DbgAddrPDP10} dbgAddr + * @return {string|null} CALL instruction at or near dbgAddr, or null if none + */ + getCall(dbgAddr) + { + var sCall = null; + var addr = dbgAddr.addr; + var addrOrig = addr; + for (var n = 1; n <= 6 && !!addr; n++) { + if (n > 2) { + dbgAddr.addr = addr; + var s = this.getInstruction(dbgAddr); + if (s.indexOf("JSR") >= 0) { + /* + * Verify that the length of this call, when added to the address of the call, matches + * the original return address. We do this by getting the string index of the opcode bytes, + * subtracting that from the string index of the next space, and dividing that difference + * by two, to yield the length of the CALL (or INT) instruction, in bytes. + */ + var i = s.indexOf(' '); + var j = s.indexOf(' ', i+1); + if (addr + (j - i - 1)/2 == addrOrig) { + sCall = s; + break; + } + } + } + addr -= 2; + } + dbgAddr.addr = addrOrig; + return sCall; + } + + /** + * doStackTrace(sCmd, sAddr) + * + * Use "k" for a normal stack trace and "ks" for a stack trace with symbolic info. + * + * @this {DebuggerPDP10} + * @param {string} [sCmd] + * @param {string} [sAddr] (not used yet) + */ + doStackTrace(sCmd, sAddr) + { + if (sAddr == '?') { + this.println("stack trace commands:"); + this.println("\tk\tshow frame addresses"); + this.println("\tks\tshow symbol information"); + return; + } + + var nFrames = 10, cFrames = 0; + var dbgAddrCall = this.newAddr(); + var dbgAddrStack = this.newAddr(/*this.cpu.getSP()*/); + this.println("stack trace for " + this.toStrAddr(dbgAddrStack)); + + while (cFrames < nFrames) { + var sCall = null, sCallPrev = null, cTests = 256; + while ((dbgAddrStack.addr >>> 0) < 0x10000) { + dbgAddrCall.addr = this.getWord(dbgAddrStack, 2); + /* + * Because we're using the auto-increment feature of getWord(), and because that will automatically + * wrap the offset around the end of the segment, we must also check the addr property to detect the wrap. + */ + if (dbgAddrStack.addr == null || !cTests--) break; + if (dbgAddrCall.addr & 0x1) continue; // an odd address on the PDP-11 is not a valid instruction boundary + sCall = this.getCall(dbgAddrCall); + if (sCall) break; + } + /* + * The sCallPrev check eliminates duplicate sequential calls, which are usually (but not always) + * indicative of a false positive, in which case the previous call is probably bogus as well, but + * at least we won't duplicate that mistake. Of course, there are always exceptions, recursion + * being one of them, but it's rare that we're debugging recursive code. + */ + if (!sCall || sCall == sCallPrev) break; + var sSymbol = null; + if (sCmd == "ks") { + var a = sCall.match(/[0-9A-F]+$/); + if (a) sSymbol = this.doList(a[0]); + } + sCall = Str.pad(sCall, 50) + " ;" + (sSymbol || "stack=" + this.toStrAddr(dbgAddrStack)); // + " return=" + this.toStrAddr(dbgAddrCall)); + this.println(sCall); + sCallPrev = sCall; + cFrames++; + } + if (!cFrames) this.println("no return addresses found"); + } + + /** + * doTrace(sCmd, sCount) + * + * The "t" and "tr" commands interpret the count as a number of instructions, and since + * we call the Debugger's stepCPU() for each iteration, a single instruction includes + * any/all prefixes; the CPU's stepCPU() treats prefixes as discrete operations. The only + * difference between "t" and "tr": the former displays only the next instruction, while + * the latter also displays the (updated) registers. + * + * The "tc" command interprets the count as a number of cycles rather than instructions, + * allowing you to quickly execute large chunks of instructions with a single command; it + * doesn't display anything until the the chunk has finished. "tc 1" is also a useful + * command in that it doesn't inhibit interrupts like "t" or "tr" does. + * + * However, generally a more useful command is "bn", which allows you to break after some + * number of instructions have been executed (as opposed to some number of cycles). + * + * @this {DebuggerPDP10} + * @param {string} [sCmd] ("t", "tc", or "tr") + * @param {string} [sCount] # of instructions to step + */ + doTrace(sCmd, sCount) + { + if (sCount == '?') { + this.println("trace commands:"); + this.println("\tt [#]\ttrace # instructions"); + this.println("\ttr [#]\ttrace # instructions with register updates"); + this.println("\ttc [#]\ttrace # cycles"); + this.println("note: bn [#] breaks after # instructions without updates"); + return; + } + + var dbg = this; + var fRegs = (sCmd != "t"); + var nCount = this.parseValue(sCount, null, true) || 1; + + /* + * We used to set nCycles to 1 when a count > 1 was specified, because nCycles set + * to 0 used to mean "execute the next instruction without checking for interrupts". + * Well, this machine's stepCPU() doesn't do that; it ALWAYS checks for interrupts, + * so we should leave nCycles set to 0, so that if an interrupt is dispatched, we will + * get to see the first instruction of the interrupt handler. + */ + var nCycles = 0; // (nCount == 1? 0 : 1); + + if (sCmd == "tc") { + nCycles = nCount; + nCount = 1; + } + this.sCmdTracePrev = sCmd; + + Web.onCountRepeat( + nCount, + function onCountStep() { + return dbg.setBusy(true) && dbg.stepCPU(nCycles, fRegs, false); + }, + function onCountStepComplete() { + /* + * We explicitly called stepCPU() with fUpdateDisplays set to false, because repeatedly + * calling updateDisplays() can be very slow, especially if a Control Panel is present with + * displayLiveRegs enabled, so once the repeat count has been exhausted, we must perform + * a final updateDisplays(). + */ + if (dbg.panel) dbg.panel.stop(); + dbg.cmp.updateDisplays(-1); + dbg.setBusy(false); + } + ); + } + + /** + * doUnassemble(sAddr, sAddrEnd, nLines) + * + * @this {DebuggerPDP10} + * @param {string} [sAddr] + * @param {string} [sAddrEnd] + * @param {number} [nLines] + */ + doUnassemble(sAddr, sAddrEnd, nLines) + { + var dbgAddr = this.parseAddr(sAddr, true); + if (!dbgAddr) return; + + if (nLines === undefined) nLines = 1; + + var nBytes = 0x100; + if (sAddrEnd !== undefined) { + + if (sAddrEnd.charAt(0) == 'l') { + var n = this.parseValue(sAddrEnd.substr(1)); + if (n != null) nLines = n; + } + else { + var dbgAddrEnd = this.parseAddr(sAddrEnd, true); + if (!dbgAddrEnd || dbgAddrEnd.addr < dbgAddr.addr) return; + + nBytes = dbgAddrEnd.addr - dbgAddr.addr; + if (!DEBUG && nBytes > 0x100) { + /* + * Limiting the amount of disassembled code to 256 bytes in non-DEBUG builds is partly to + * prevent the user from wedging the browser by dumping too many lines, but also a recognition + * that, in non-DEBUG builds, this.println() keeps print output buffer truncated to 8Kb anyway. + */ + this.println("range too large"); + return; + } + nLines = -1; + } + } + + var nPrinted = 0; + var sInstruction; + + while (nBytes > 0 && nLines--) { + + var nSequence = (this.isBusy(false) || this.nStep)? this.nCycles : null; + var sComment = (nSequence != null? "cycles" : null); + var aSymbol = this.findSymbol(dbgAddr); + + var addr = dbgAddr.addr; // we snap dbgAddr.addr *after* calling findSymbol(), which re-evaluates it + + if (aSymbol[0] && nLines) { + if (!nPrinted && nLines || aSymbol[0].indexOf('+') < 0) { + var sLabel = aSymbol[0] + ':'; + if (aSymbol[2]) sLabel += ' ' + aSymbol[2]; + this.println(sLabel); + } + } + + if (aSymbol[3]) { + sComment = aSymbol[3]; + nSequence = null; + } + + sInstruction = this.getInstruction(dbgAddr, sComment, nSequence); + + this.println(sInstruction); + this.dbgAddrNextCode = dbgAddr; + nBytes -= dbgAddr.addr - addr; + nPrinted++; + } + } + + /** + * splitArgs(sCmd) + * + * @this {DebuggerPDP10} + * @param {string} sCmd + * @return {Array.} + */ + splitArgs(sCmd) + { + var asArgs = sCmd.replace(/ +/g, ' ').split(' '); + asArgs[0] = asArgs[0].toLowerCase(); + if (asArgs && asArgs.length) { + var s0 = asArgs[0]; + var ch0 = s0.charAt(0); + for (var i = 1; i < s0.length; i++) { + var ch = s0.charAt(i); + if (ch0 == '?' || ch0 == 'r' || ch < 'a' || ch > 'z') { + asArgs[0] = s0.substr(i); + asArgs.unshift(s0.substr(0, i)); + break; + } + } + } + return asArgs; + } + + /** + * doCommand(sCmd, fQuiet) + * + * @this {DebuggerPDP10} + * @param {string} sCmd + * @param {boolean} [fQuiet] + * @return {boolean} true if command processed, false if unrecognized + */ + doCommand(sCmd, fQuiet) + { + var result = true; + + try { + if (!sCmd.length || sCmd == "end") { + if (this.fAssemble) { + this.println("ended assemble at " + this.toStrAddr(this.dbgAddrAssemble)); + this.dbgAddrNextCode = this.dbgAddrAssemble; + this.fAssemble = false; + } + sCmd = ""; + } + else if (!fQuiet) { + this.println(DebuggerPDP10.PROMPT + sCmd); + } + + var ch = sCmd.charAt(0); + if (ch == '"' || ch == "'") return true; + + /* + * Zap the previous message buffer to ensure the new command's output is not tossed out as a repeat. + */ + this.sMessagePrev = null; + + /* + * I've relaxed the !isBusy() requirement, to maximize our ability to issue Debugger commands externally. + */ + if (this.isReady() /* && !this.isBusy(true) */ && sCmd.length > 0) { + + if (this.fAssemble) { + sCmd = "a " + this.toStrAddr(this.dbgAddrAssemble) + ' ' + sCmd; + } + + var fError = false; + var asArgs = this.splitArgs(sCmd); + + switch (asArgs[0].charAt(0)) { + case 'a': + this.doAssemble(asArgs); + break; + case 'b': + this.doBreak(asArgs[0], asArgs[1], sCmd); + break; + case 'c': + this.doClear(asArgs[0]); + break; + case 'd': + if (!COMPILED && sCmd == "debug") { + window.DEBUG = true; + this.println("DEBUG checks on"); + break; + } + this.doDump(asArgs); + break; + case 'e': + if (asArgs[0] == "else") break; + this.doEdit(asArgs); + break; + case 'g': + this.doRun(asArgs[0], asArgs[1], sCmd, fQuiet); + break; + case 'h': + this.doHalt(fQuiet); + break; + case 'i': + if (asArgs[0] == "if") { + if (!this.doIf(sCmd.substr(2), fQuiet)) { + result = false; + } + break; + } + fError = true; + break; + case 'k': + this.doStackTrace(asArgs[0], asArgs[1]); + break; + case 'l': + if (asArgs[0] == "ln") { + this.doList(asArgs[1], true); + break; + } + fError = true; + break; + case 'm': + this.doMessages(asArgs); + break; + case 'p': + if (asArgs[0] == "print") { + this.doPrint(sCmd.substr(5)); + break; + } + this.doStep(asArgs[0], asArgs[1]); + break; + case 'r': + if (sCmd == "reset") { + if (this.cmp) this.cmp.reset(); + break; + } + this.doRegisters(asArgs); + break; + case 's': + this.doOptions(asArgs); + break; + case 't': + this.doTrace(asArgs[0], asArgs[1]); + break; + case 'u': + this.doUnassemble(asArgs[1], asArgs[2], 8); + break; + case 'v': + if (asArgs[0] == "var") { + if (!this.doVar(sCmd.substr(3))) { + result = false; + } + break; + } + if (asArgs[0] == "ver") { + this.println((PDP10.APPNAME || "PDP10") + " version " + (XMLVERSION || PDP10.APPVERSION) + " (" + this.cpu.model + (PDP10.COMPILED? ",RELEASE" : (PDP10.DEBUG? ",DEBUG" : ",NODEBUG")) + ')'); + this.println(Web.getUserAgent()); + break; + } + fError = true; + break; + case '?': + if (asArgs[1]) { + this.doPrint(sCmd.substr(1)); + break; + } + this.doHelp(); + break; + case 'n': + if (!COMPILED && sCmd == "nodebug") { + window.DEBUG = false; + this.println("DEBUG checks off"); + break; + } + if (this.doInfo(asArgs)) break; + /* falls through */ + default: + fError = true; + break; + } + if (fError) { + this.println("unknown command: " + sCmd); + result = false; + } + } + } catch(e) { + this.println("debugger error: " + (e.stack || e.message)); + result = false; + } + return result; + } + + /** + * doCommands(sCmds, fSave) + * + * @this {DebuggerPDP10} + * @param {string} sCmds + * @param {boolean} [fSave] + * @return {boolean} true if all commands processed, false if not + */ + doCommands(sCmds, fSave) + { + var a = this.parseCommand(sCmds, fSave); + for (var s in a) { + if (!this.doCommand(a[+s])) return false; + } + return true; + } + + /** + * DebuggerPDP10.init() + * + * This function operates on every HTML element of class "debugger", extracting the + * JSON-encoded parameters for the Debugger constructor from the element's "data-value" + * attribute, invoking the constructor to create a Debugger component, and then binding + * any associated HTML controls to the new component. + */ + static init() + { + var aeDbg = Component.getElementsByClass(document, PDP10.APPCLASS, "debugger"); + for (var iDbg = 0; iDbg < aeDbg.length; iDbg++) { + var eDbg = aeDbg[iDbg]; + var parmsDbg = Component.getComponentParms(eDbg); + var dbg = new DebuggerPDP10(parmsDbg); + Component.bindComponentControls(dbg, eDbg, PDP10.APPCLASS); + } + } +} + +if (DEBUGGER) { + + /* + * NOTE: Every DebuggerPDP10 property from here to the first prototype function definition (initBus()) is + * considered a "class constant"; most of them use our "all-caps" convention (and all of them SHOULD, but + * that wouldn't help us catch any bugs). + * + * Technically, all of them should ALSO be preceded by a "@const" annotation, but that's a lot of work and it + * really clutters the code. I wish the Closure Compiler had a way to annotate every definition with a given + * section with a single annotation.... + */ + + DebuggerPDP10.COMMANDS = { + '?': "help/print", + 'a [#]': "assemble", // TODO: Implement this command someday + 'b [#]': "breakpoint", // multiple variations (use b? to list them) + 'c': "clear output", + 'd [#]': "dump memory", // additional syntax: d [#] [l#], where l# is a number of bytes to dump + 'e [#]': "edit memory", + 'g [#]': "go [to #]", + 'h': "halt", + 'if': "eval expression", + 'int [#]': "request interrupt", + 'k': "stack trace", + "ln": "list nearest symbol(s)", + 'm': "messages", + 'p': "step over", // other variations: pr (step and dump registers) + 'print': "print expression", + 'r': "dump/set registers", + 'reset': "reset machine", + 's': "set options", + 't [#]': "trace", // other variations: tr (trace and dump registers) + 'u [#]': "unassemble", + 'var': "assign variable", + 'ver': "print version" + }; + + /* + * CPU opcode IDs + */ + DebuggerPDP10.OPS = { + NONE: 0 + }; + + /* + * CPU opcode names, indexed by CPU opcode ordinal (above) + */ + DebuggerPDP10.OPNAMES = [ + ".WORD", + ]; + + DebuggerPDP10.REGS = { + PC: 0 + }; + + DebuggerPDP10.REGNAMES = [ + "PC" + ]; + + /* + * The OPTABLE contains opcode masks, and each mask refers to table of possible values, and each + * value refers to an array that contains: + * + * [0]: {number} of the opcode name (see OP.*) + * [1]: {number} containing the first operand type bit(s), if any + * [2]: {number} containing the second operand type bit(s), if any + * + * Note that, by convention, opcodes that require two operands list the SRC operand first and DST operand + * second (ie, the OPPOSITE of the Intel convention). + * + * Also note that, for some of the newer PDP-11 opcodes (eg, MUL, DIV, ASH, ASHC), the location of the + * opcode's SRC and DST bits are reversed. This is why, for example, you'll see the MUL instruction defined + * below as having OP_DST for the first operand and OP_SRCREG for the second operand. This does NOT mean + * that the opcode's destination operand is being listed first, but rather that the bits describing the source + * operand are in the opcode's OP_DST field. + */ + DebuggerPDP10.OPTABLE = {}; + + DebuggerPDP10.OPNONE = [DebuggerPDP10.OPS.NONE]; + + DebuggerPDP10.HISTORY_LIMIT = DEBUG? 100000 : 1000; + + DebuggerPDP10.PROMPT = ">> "; + + /* + * Initialize every Debugger module on the page (as IF there's ever going to be more than one ;-)) + */ + Web.onInit(DebuggerPDP10.init); + +} // endif DEBUGGER + +if (NODE) module.exports = DebuggerPDP10; diff --git a/modules/pdp10/lib/defines.js b/modules/pdp10/lib/defines.js new file mode 100644 index 000000000..2560419e3 --- /dev/null +++ b/modules/pdp10/lib/defines.js @@ -0,0 +1,148 @@ +/** + * @fileoverview PDP10-specific compile-time definitions. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +/** + * @define {string} + */ +var APPCLASS = "pdp10"; // this @define is the default application class (eg, "pcx86", "c1pjs") + +/** + * APPNAME is used more for display purposes than anything else now. APPCLASS is what matters in terms + * of folder and file names, CSS styles, etc. + * + * @define {string} + */ +var APPNAME = "PDPjs"; // this @define is the default application name (eg, "PCx86", "C1Pjs") + +/** + * WARNING: DEBUGGER needs to accurately reflect whether or not the Debugger component is (or will be) loaded. + * In the compiled case, we rely on the Closure Compiler to override DEBUGGER as appropriate. When it's *false*, + * nearly all of debugger.js will be conditionally removed by the compiler, reducing it to little more than a + * "type skeleton", which also solves some type-related warnings we would otherwise have if we tried to remove + * debugger.js from the compilation process altogether. + * + * However, when we're in "development mode" and running uncompiled code in debugger-less configurations, + * I would like to skip loading debugger.js altogether. When doing that, we must ALSO arrange for an additional file + * (nodebugger.js) to be loaded immediately after this file, which *explicitly* overrides DEBUGGER with *false*. + * + * @define {boolean} + */ +var DEBUGGER = true; // this @define is overridden by the Closure Compiler to remove Debugger-related support + +/* + * Combine all the shared globals and machine-specific globals into one machine-specific global object, + * which all machine components should start using; eg: "if (PDP10.DEBUG) ..." instead of "if (DEBUG) ...". + */ +var PDP10 = { + APPCLASS: APPCLASS, + APPNAME: APPNAME, + APPVERSION: APPVERSION, // shared + COMPILED: COMPILED, // shared + CSSCLASS: CSSCLASS, // shared + DEBUG: DEBUG, // shared + DEBUGGER: DEBUGGER, + MAXDEBUG: MAXDEBUG, // shared + PRIVATE: PRIVATE, // shared + SITEHOST: SITEHOST, // shared + XMLVERSION: XMLVERSION, // shared + + /* + * CPU model numbers (supported) + * + * The 11/20 includes the 11/10, which is not identified separately because there was + * nothing functionally different about it. + * + * The 11/40 added the MODE bits to the PSW (but only KERNEL=00 and USER=11) and 18-bit + * addressing via an MMU; there was still only one register set. + * + * The 11/45 added REGSET bit to the PSW (to support a second register set), SUPER=01 + * mode to the existing KERNEL=00 and USER=11 modes, separate I/D spaces, and other MMU + * extensions (eg, MMR1 and MMR3). + * + * The 11/70 added 22-bit addressing and corresponding extensions to the MMU. + */ + MODEL_KA10: 1001, + + /* + * This constant is used to mark points in the code where the physical address being returned + * is invalid and should not be used. + * + * In a 32-bit CPU, -1 (ie, 0xffffffff) could actually be a valid address, so consider changing + * ADDR_INVALID to NaN or null (which is also why all ADDR_INVALID tests should use strict equality + * operators). + * + * The main reason I'm NOT using NaN or null now is my concern that, by mixing non-numbers + * (specifically, values outside the range of signed 32-bit integers), performance may suffer. + * + * WARNING: Like many of the properties defined here, ADDR_INVALID is a common constant, which the + * Closure Compiler will happily inline (with or without @const annotations; in fact, I've yet to + * see a @const annotation EVER improve automatic inlining). However, if you don't make ABSOLUTELY + * certain that this file is included BEFORE the first reference to any of these properties, that + * automatic inlining will no longer occur. + */ + ADDR_INVALID: -1, + ADDR_LIMIT: Math.pow(2, 18), + DATA_INVALID: 0, + DATA_LIMIT: Math.pow(2, 36), + /* + * Assorted common opcodes + */ + OPCODE: { + HALT: 0o000000000000, // TODO: Resolve + INVALID: 0o777777777777 // TODO: Resolve + }, + /* + * Internal operation state flags + */ + OPFLAG: { + IRQ_DELAY: 0x0001, // incremented until it becomes IRQ + IRQ: 0x0002, // time to call checkInterrupts() + IRQ_MASK: 0x0003, + DEBUGGER: 0x0004, // set if the Debugger wants to perform checks + WAIT: 0x0008, // WAIT operation in progress + PRESERVE: 0x000F, // OPFLAG bits to preserve prior to the next instruction + } +}; + +/* + * Combine all the shared globals and machine-specific globals into one machine-specific global object, + * which all machine components should start using; eg: "if (PDP10.DEBUGGER)" instead of "if (DEBUGGER)". + */ +PDP10.APPCLASS = APPCLASS; +PDP10.APPNAME = APPNAME; +PDP10.DEBUGGER = DEBUGGER; + +if (NODE) { + global.APPCLASS = APPCLASS; + global.APPNAME = APPNAME; + global.DEBUGGER = DEBUGGER; + global.PDP10 = PDP10; + module.exports = PDP10; +} diff --git a/modules/pdp10/lib/device.js b/modules/pdp10/lib/device.js new file mode 100644 index 000000000..b9d918740 --- /dev/null +++ b/modules/pdp10/lib/device.js @@ -0,0 +1,172 @@ +/** + * @fileoverview Implements PDP-10 device support. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +if (NODE) { + var Str = require("../../shared/lib/strlib"); + var Web = require("../../shared/lib/weblib"); + var Component = require("../../shared/lib/component"); + var State = require("../../shared/lib/state"); + var PDP10 = require("./defines"); + var BusPDP10 = require("./bus"); + var MemoryPDP10 = require("./memory"); + var MessagesPDP10 = require("./messages"); +} + +class DevicePDP10 extends Component { + /** + * DevicePDP10(parmsDevice) + * + * @param {Object} parmsDevice + */ + constructor(parmsDevice) + { + super("Device", parmsDevice, MessagesPDP10.DEVICE); + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * @this {DevicePDP10} + * @param {ComputerPDP10} cmp + * @param {BusPDP10} bus + * @param {CPUStatePDP10} cpu + * @param {DebuggerPDP10} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.bus = bus; + this.cmp = cmp; + this.cpu = cpu; + this.dbg = dbg; + + this.setReady(); + } + + /** + * powerUp(data, fRepower) + * + * @this {DevicePDP10} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (!fRepower) { + if (!data) { + this.reset(); + } else { + if (!this.restore(data)) return false; + } + } + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * @this {DevicePDP10} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + return fSave? this.save() : true; + } + + /** + * reset() + * + * @this {DevicePDP10} + */ + reset() + { + } + + /** + * save() + * + * This implements save support for the DevicePDP10 component. + * + * @this {DevicePDP10} + * @return {Object} + */ + save() + { + var state = new State(this); + return state.data(); + } + + /** + * restore(data) + * + * This implements restore support for the DevicePDP10 component. + * + * @this {DevicePDP10} + * @param {Object} data + * @return {boolean} true if successful, false if failure + */ + restore(data) + { + return true; + } + + /** + * DevicePDP10.init() + * + * This function operates on every HTML element of class "device", extracting the + * JSON-encoded parameters for the DevicePDP10 constructor from the element's "data-value" + * attribute, invoking the constructor to create a DevicePDP10 component, and then binding + * any associated HTML controls to the new component. + */ + static init() + { + var aeDevice = Component.getElementsByClass(document, PDP10.APPCLASS, "device"); + for (var iDevice = 0; iDevice < aeDevice.length; iDevice++) { + var device; + var eDevice = aeDevice[iDevice]; + var parmsDevice = Component.getComponentParms(eDevice); + switch(parmsDevice['type']) { + case 'default': + device = new DevicePDP10(parmsDevice); + Component.bindComponentControls(device, eDevice, PDP10.APPCLASS); + break; + } + } + } +} + +/* + * Initialize all the DevicePDP10 modules on the page. + */ +Web.onInit(DevicePDP10.init); + +if (NODE) module.exports = DevicePDP10; diff --git a/modules/pdp10/lib/memory.js b/modules/pdp10/lib/memory.js new file mode 100644 index 000000000..7381b68f5 --- /dev/null +++ b/modules/pdp10/lib/memory.js @@ -0,0 +1,645 @@ +/** + * @fileoverview Implements the PDP-10 Memory component. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +if (NODE) { + var Component = require("../../shared/lib/component"); + var Int36 = require("../../shared/lib/int36"); + var PDP10 = require("./defines"); + var MessagesPDP10 = require("./messages"); +} + +/** + * @class DataView + * @property {function(number,boolean):number} getUint8 + * @property {function(number,number,boolean)} setUint8 + * @property {function(number,boolean):number} getUint16 + * @property {function(number,number,boolean)} setUint16 + * @property {function(number,boolean):number} getInt32 + * @property {function(number,number,boolean)} setInt32 + */ +class MemoryPDP10 { + /** + * MemoryPDP10(bus, addr, used, size, type) + * + * The Bus component allocates Memory objects so that each has a memory buffer with a + * block-granular starting address and an address range equal to bus.nBlockSize; however, + * the size of any given Memory object's underlying buffer can be either zero or bus.nBlockSize; + * memory read/write functions for empty (buffer-less) blocks are mapped to readNone/writeNone. + * + * The Bus allocates empty blocks for the entire address space during initialization, so that + * any reads/writes to undefined addresses will have no effect. Later, the ROM and RAM + * components will ask the Bus to allocate memory for specific ranges, and the Bus will allocate + * as many new blockSize Memory objects as the ranges require. Partial Memory blocks could + * also be supported in theory, but in practice, they're not. + * + * WARNING: Since Memory blocks are low-level objects that have no UI requirements, they + * do not inherit from the Component class, so if you want to use any Component class methods, + * such as Component.assert(), use the corresponding Debugger methods instead (assuming a debugger + * is available). + * + * @param {BusPDP10} bus + * @param {number|null} [addr] of lowest used address in block + * @param {number} [used] portion of block in words (0 for none) + * @param {number} [size] of block's buffer in words (0 for none) + * @param {number} [type] is one of the MemoryPDP10.TYPE constants (default is MemoryPDP10.TYPE.NONE) + */ + constructor(bus, addr, used, size, type) + { + var a, i; + this.bus = bus; + this.id = (MemoryPDP10.idBlock += 2); + this.adw = null; + this.offset = 0; + this.addr = addr; + this.used = used; + this.size = size || 0; + this.type = type || MemoryPDP10.TYPE.NONE; + this.fReadOnly = (type == MemoryPDP10.TYPE.ROM); + this.dbg = null; + this.readBits = this.readBitsDirect = this.readNone; + this.readWord = this.readWordDirect = this.readWordDefault; + this.writeBits = this.writeBitsDirect = this.writeNone; + this.writeWord = this.writeWordDirect = this.writeWordDefault; + this.cReadBreakpoints = this.cWriteBreakpoints = 0; + this.copyBreakpoints(); // initialize the block's Debugger info; the caller will reinitialize + + /* + * TODO: Study the impact of dirty block tracking. The original purposes were to allow saveMemory() + * to save only dirty blocks, and to enable the Video component to quickly detect changes to the video buffer. + * + * However, a quick test with dirty block tracking disabled didn't yield a noticeable improvement in performance, + * so I think the overhead of our block-based architecture is swamping the impact of these micro-updates. + */ + this.fDirty = this.fDirtyEver = false; + + /* + * For empty memory blocks, all we need to do is ensure all access functions are mapped to "none" handlers. + */ + if (!this.size) { + this.setAccess(); + return; + } + + /* + * This is the normal case: allocate a buffer that provides a word of data per address; + * no controller is required because our default memory access functions (see afnMemory) + * know how to deal with this simple 1-1 mapping of addresses to words. + * + * TODO: Consider initializing the memory array to random (or pseudo-random) values in DEBUG + * mode; pseudo-random might be best, to help make any bugs reproducible. + */ + a = this.aw = new Array(this.size); + for (i = 0; i < a.length; i++) a[i] = 0; + this.setAccess(MemoryPDP10.afnMemory); + } + + /** + * init(addr) + * + * Quick reinitializer when reusing a Memory block. + * + * @this {MemoryPDP10} + * @param {number} addr + */ + init(addr) + { + this.addr = addr; + } + + /** + * clone(mem, type, dbg) + * + * Converts the current Memory block (this) into a clone of the given Memory block (mem), + * and optionally overrides the current block's type with the specified type. + * + * @this {MemoryPDP10} + * @param {MemoryPDP10} mem + * @param {number} [type] + * @param {DebuggerPDP10} [dbg] + */ + clone(mem, type, dbg) + { + /* + * Original memory block IDs are even; cloned memory block IDs are odd; + * the original ID of the current block is lost, but that's OK, since it was presumably + * produced merely to become a clone. + */ + this.id = mem.id | 0x1; + this.used = mem.used; + this.size = mem.size; + if (type) { + this.type = type; + this.fReadOnly = (type == MemoryPDP10.TYPE.ROM); + } + this.aw = mem.aw; + this.setAccess(MemoryPDP10.afnMemory); + this.copyBreakpoints(dbg, mem); + } + + /** + * save() + * + * This gets the contents of a Memory block as an array of numeric values; used by Bus.saveMemory(), + * which in turn is called by CPUState.save(). + * + * @this {MemoryPDP10} + * @return {Array.|null} + */ + save() + { + return this.aw; + } + + /** + * restore(aw) + * + * This restores the contents of a Memory block from an array of numeric values; used by Bus.restoreMemory(), + * which is called by CPUState.restore(), after all other components have been restored and thus all Memory + * blocks have been allocated by their respective components. + * + * @this {MemoryPDP10} + * @param {Array.|null} aw + * @return {boolean} true if successful, false if block size mismatch + */ + restore(aw) + { + if (aw && this.size == aw.length) { + this.aw = aw; + this.fDirty = true; + return true; + } + return false; + } + + /** + * zero(off, len, pattern) + * + * @this {MemoryPDP10} + * @param {number} [off] (optional starting word offset within block) + * @param {number} [len] (optional maximum number of words; default is the entire block) + * @param {number} [pattern] + */ + zero(off, len, pattern) + { + var i; + off = off || 0; + pattern = Int36.validate(pattern || 0); + /* + * NOTE: If len happens to be larger than the block, that's OK, because we also bounds-check the index. + */ + if (len === undefined) len = this.size; + Component.assert(off >= 0 && off < this.size); + for (i = off; len-- && i < this.size; i++) this.writeWordDirect(off, pattern, this.addr + off); + } + + /** + * setAccess(afn, fDirect) + * + * The afn parameter should be a 4-entry function table containing two bits handlers and + * two word handlers. See the static afnMemory table for an example. + * + * If no function table is specified, a default is selected based on the Memory type; + * similarly, any undefined entries in the table are filled with default handlers that fall + * back to the bits handlers, and if one or both bits handlers are undefined, they default + * to handlers that simply ignore the access. + * + * fDirect indicates that both the default AND the direct handlers should be updated. Direct + * handlers normally match the default handlers, except when "checked" handlers are installed; + * this allows "checked" handlers to know where to dispatch the call after performing checks. + * Examples of checks are read/write breakpoints, but it's really up to the Debugger to decide + * what the check consists of. + * + * @this {MemoryPDP10} + * @param {Array.} [afn] function table + * @param {boolean} [fDirect] (true to update direct access functions as well; default is true) + */ + setAccess(afn, fDirect) + { + if (!afn) { + Component.assert(this.type == MemoryPDP10.TYPE.NONE); + afn = MemoryPDP10.afnNone; + } + this.setReadAccess(afn, fDirect); + this.setWriteAccess(afn, fDirect); + } + + /** + * setReadAccess(afn, fDirect) + * + * @this {MemoryPDP10} + * @param {Array.} afn + * @param {boolean} [fDirect] + */ + setReadAccess(afn, fDirect) + { + if (!fDirect || !this.cReadBreakpoints) { + this.readBits = afn[0] || this.readNone; + this.readWord = afn[2] || this.readWordDefault; + } + if (fDirect || fDirect === undefined) { + this.readBitsDirect = afn[0] || this.readNone; + this.readWordDirect = afn[2] || this.readWordDefault; + } + } + + /** + * setWriteAccess(afn, fDirect) + * + * @this {MemoryPDP10} + * @param {Array.} afn + * @param {boolean} [fDirect] + */ + setWriteAccess(afn, fDirect) + { + if (!fDirect || !this.cWriteBreakpoints) { + this.writeBits = !this.fReadOnly && afn[1] || this.writeNone; + this.writeWord = !this.fReadOnly && afn[3] || this.writeWordDefault; + } + if (fDirect || fDirect === undefined) { + this.writeBitsDirect = afn[1] || this.writeNone; + this.writeWordDirect = afn[3] || this.writeWordDefault; + } + } + + /** + * resetReadAccess() + * + * @this {MemoryPDP10} + */ + resetReadAccess() + { + this.readBits = this.readBitsDirect; + this.readWord = this.readWordDirect; + } + + /** + * resetWriteAccess() + * + * @this {MemoryPDP10} + */ + resetWriteAccess() + { + this.writeBits = this.fReadOnly? this.writeNone : this.writeBitsDirect; + this.writeWord = this.fReadOnly? this.writeWordDefault : this.writeWordDirect; + } + + /** + * printAddr(sMessage) + * + * @this {MemoryPDP10} + * @param {string} sMessage + */ + printAddr(sMessage) + { + if (DEBUG && this.dbg && this.dbg.messageEnabled(MessagesPDP10.MEMORY)) { + this.dbg.printMessage(sMessage + ' ' + (this.addr != null? ('@' + this.dbg.toStrBase(this.addr)) : '#' + this.id), true); + } + } + + /** + * addBreakpoint(off, fWrite) + * + * @this {MemoryPDP10} + * @param {number} off + * @param {boolean} fWrite + */ + addBreakpoint(off, fWrite) + { + if (!fWrite) { + if (this.cReadBreakpoints++ === 0) { + this.setReadAccess(MemoryPDP10.afnChecked, false); + } + if (DEBUG) this.printAddr("read breakpoint added to memory block"); + } + else { + if (this.cWriteBreakpoints++ === 0) { + this.setWriteAccess(MemoryPDP10.afnChecked, false); + } + if (DEBUG) this.printAddr("write breakpoint added to memory block"); + } + } + + /** + * removeBreakpoint(off, fWrite) + * + * @this {MemoryPDP10} + * @param {number} off + * @param {boolean} fWrite + */ + removeBreakpoint(off, fWrite) + { + if (!fWrite) { + if (--this.cReadBreakpoints === 0) { + this.resetReadAccess(); + if (DEBUG) this.printAddr("all read breakpoints removed from memory block"); + } + Component.assert(this.cReadBreakpoints >= 0); + } + else { + if (--this.cWriteBreakpoints === 0) { + this.resetWriteAccess(); + if (DEBUG) this.printAddr("all write breakpoints removed from memory block"); + } + Component.assert(this.cWriteBreakpoints >= 0); + } + } + + /** + * copyBreakpoints(dbg, mem) + * + * @this {MemoryPDP10} + * @param {DebuggerPDP10} [dbg] + * @param {MemoryPDP10} [mem] (outgoing MemoryPDP10 block to copy breakpoints from, if any) + */ + copyBreakpoints(dbg, mem) + { + this.dbg = dbg; + this.cReadBreakpoints = this.cWriteBreakpoints = 0; + if (mem) { + if ((this.cReadBreakpoints = mem.cReadBreakpoints)) { + this.setReadAccess(MemoryPDP10.afnChecked, false); + } + if ((this.cWriteBreakpoints = mem.cWriteBreakpoints)) { + this.setWriteAccess(MemoryPDP10.afnChecked, false); + } + } + } + + /** + * readNone(off) + * + * @this {MemoryPDP10} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readNone(off, addr) + { + if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP10.MEMORY) /* && !off */) { + this.dbg.printMessage("attempt to read invalid address " + this.dbg.toStrBase(addr), true); + } + return 0; + } + + /** + * writeNone(v, off, addr) + * + * @this {MemoryPDP10} + * @param {number} v + * @param {number} off + * @param {number} addr + */ + writeNone(v, off, addr) + { + if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP10.MEMORY) /* && !off */) { + this.dbg.printMessage("attempt to write " + this.dbg.toStrBase(v) + " to invalid addresses " + this.dbg.toStrBase(addr), true); + } + } + + /** + * readWordDefault(off, addr) + * + * @this {MemoryPDP10} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readWordDefault(off, addr) + { + return this.readWord(off, addr); + } + + /** + * writeWordDefault(w, off, addr) + * + * @this {MemoryPDP10} + * @param {number} w + * @param {number} off + * @param {number} addr + */ + writeWordDefault(w, off, addr) + { + this.writeWord(w, off, addr); + } + + /** + * readBitsMemory(offBits, lenBits, off, addr) + * + * @this {MemoryPDP10} + * @param {number} offBits (the bit position of the right-most bit of the result, using modern bit numbering) + * @param {number} lenBits + * @param {number} off + * @param {number} addr + * @return {number} + */ + readBitsMemory(offBits, lenBits, off, addr) + { + var w = this.aw[off]; + if (offBits + lenBits <= 32) { + w = (w >> offBits) & ((1 << lenBits) - 1); + } else { + w = Math.trunc(w / Math.pow(2, offBits)) % Math.pow(2, lenBits); + } + return w; + } + + /** + * readWordMemory(off, addr) + * + * @this {MemoryPDP10} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readWordMemory(off, addr) + { + return this.aw[off]; + } + + /** + * writeBitsMemory(offBits, lenBits, bits, off, addr) + * + * @this {MemoryPDP10} + * @param {number} offBits (the bit position of the right-most bit of the result, using modern bit numbering) + * @param {number} lenBits + * @param {number} bits (only the right-most lenBits of bits are used, so this value doesn't need to be pre-masked) + * @param {number} off + * @param {number} addr + */ + writeBitsMemory(offBits, lenBits, bits, off, addr) + { + var w = this.aw[off]; + if (offBits + lenBits <= 32) { + var bitsMask = ((1 << lenBits) - 1) << offBits; + w = (w & ~bitsMask) | ((bits << offBits) & bitsMask); + } else { + var shiftBits = Math.pow(2, offBits); + bits %= Math.pow(2, lenBits); + var v = (w % Math.pow(2, offBits + lenBits)); + w = (w - v) + (bits * shiftBits) + (v % shiftBits); + } + if (this.aw[off] != w) { + this.aw[off] = w; + this.fDirty = true; + } + } + + /** + * writeWordMemory(w, off, addr) + * + * @this {MemoryPDP10} + * @param {number} w + * @param {number} off + * @param {number} addr + */ + writeWordMemory(w, off, addr) + { + this.aw[off] = w; + this.fDirty = true; + } + + /** + * readBitsChecked(offBits, lenBits, off, addr) + * + * @this {MemoryPDP10} + * @param {number} offBits (the bit position of the right-most bit of the result, using modern bit numbering) + * @param {number} lenBits + * @param {number} off + * @param {number} addr + * @return {number} + */ + readBitsChecked(offBits, lenBits, off, addr) + { + if (DEBUGGER && this.dbg && this.addr != null) { + this.dbg.checkMemoryRead(this.addr + off); + } + return this.readBitsDirect(offBits, lenBits, off, addr); + } + + /** + * readWordChecked(off, addr) + * + * @this {MemoryPDP10} + * @param {number} off + * @param {number} addr + * @return {number} + */ + readWordChecked(off, addr) + { + if (DEBUGGER && this.dbg && this.addr != null) { + this.dbg.checkMemoryRead(this.addr + off, 2); + } + return this.readWordDirect(off, addr); + } + + /** + * writeBitsChecked(offBits, lenBits, bits, off, addr) + * + * @this {MemoryPDP10} + * @param {number} offBits (the bit position of the right-most bit of the result, using modern bit numbering) + * @param {number} lenBits + * @param {number} bits + * @param {number} off + * @param {number} addr + */ + writeBitsChecked(offBits, lenBits, bits, off, addr) + { + if (DEBUGGER && this.dbg && this.addr != null) { + this.dbg.checkMemoryWrite(this.addr + off); + } + if (this.fReadOnly) this.writeNone(bits, off, addr); else this.writeBitsDirect(offBits, lenBits, bits, off, addr); + } + + /** + * writeWordChecked(w, off, addr) + * + * @this {MemoryPDP10} + * @param {number} w + * @param {number} off + * @param {number} addr + */ + writeWordChecked(w, off, addr) + { + if (DEBUGGER && this.dbg && this.addr != null) { + this.dbg.checkMemoryWrite(this.addr + off, 2) + } + if (this.fReadOnly) this.writeNone(w, off, addr); else this.writeWordDirect(w, off, addr); + } +} + +/* + * Basic memory types + * + * RAM is the most conventional memory type, providing full read/write capability. ROM is equally + * conventional, except that the fReadOnly property is set. ROM can be written using the Bus setWordDirect() + * interface (which in turn uses the Memory writeWordDirect() interface), allowing the ROM component to + * initialize its own memory. + */ +MemoryPDP10.TYPE = { + NONE: 0, + RAM: 1, + ROM: 2 +}; +MemoryPDP10.TYPE_COLORS = ["black", "blue", "green"]; +MemoryPDP10.TYPE_NAMES = ["NONE", "RAM", "ROM"]; + +/* + * Last used block ID (used for debugging only) + */ +MemoryPDP10.idBlock = 0; + +/* + * This is the effective definition of afnNone, but we need not fully define it, because setAccess() + * uses these defaults when any of the 4 handlers (ie, 2 bits handlers and 2 word handlers) are undefined. + * +MemoryPDP10.afnNone = [ + MemoryPDP10.prototype.readNone, + MemoryPDP10.prototype.writeNone, + MemoryPDP10.prototype.readWordDefault, + MemoryPDP10.prototype.writeWordDefault +]; + */ +MemoryPDP10.afnNone = []; + +MemoryPDP10.afnMemory = [ + MemoryPDP10.prototype.readBitsMemory, + MemoryPDP10.prototype.writeBitsMemory, + MemoryPDP10.prototype.readWordMemory, + MemoryPDP10.prototype.writeWordMemory +]; + +MemoryPDP10.afnChecked = [ + MemoryPDP10.prototype.readBitsChecked, + MemoryPDP10.prototype.writeBitsChecked, + MemoryPDP10.prototype.readWordChecked, + MemoryPDP10.prototype.writeWordChecked +]; + +if (NODE) module.exports = MemoryPDP10; diff --git a/modules/pdp10/lib/messages.js b/modules/pdp10/lib/messages.js new file mode 100644 index 000000000..297d6d4e4 --- /dev/null +++ b/modules/pdp10/lib/messages.js @@ -0,0 +1,104 @@ +/** + * @fileoverview Defines PDP-10 message categories. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +var MessagesPDP10 = { + CPU: 0x00000001, + TRAP: 0x00000002, + FAULT: 0x00000004, + INT: 0x00000008, + BUS: 0x00000010, + MEMORY: 0x00000020, + MMU: 0x00000040, + ROM: 0x00000080, + DEVICE: 0x00000100, + PANEL: 0x00000200, + KEYBOARD: 0x00000400, + KEYS: 0x00000800, + PAPER: 0x00001000, + READ: 0x00004000, + WRITE: 0x00008000, + SERIAL: 0x00100000, + TIMER: 0x00200000, + SPEAKER: 0x01000000, + COMPUTER: 0x02000000, + LOG: 0x10000000, + WARN: 0x20000000, + BUFFER: 0x40000000, + HALT: 0x80000000|0 +}; + +/* + * Message categories supported by the messageEnabled() function and other assorted message + * functions. Each category has a corresponding bit value that can be combined (ie, OR'ed) as + * needed. The Debugger's message command ("m") is used to turn message categories on and off, + * like so: + * + * m port on + * m port off + * ... + * + * NOTE: The order of these categories can be rearranged, alphabetized, etc, as desired; just be + * aware that changing the bit values could break saved Debugger states (not a huge concern, just + * something to be aware of). + */ +MessagesPDP10.CATEGORIES = { + "cpu": MessagesPDP10.CPU, + "trap": MessagesPDP10.TRAP, + "fault": MessagesPDP10.FAULT, + "int": MessagesPDP10.INT, + "bus": MessagesPDP10.BUS, + "memory": MessagesPDP10.MEMORY, + "mmu": MessagesPDP10.MMU, + "rom": MessagesPDP10.ROM, + "device": MessagesPDP10.DEVICE, + "panel": MessagesPDP10.PANEL, + "keyboard": MessagesPDP10.KEYBOARD, // "kbd" is also allowed as shorthand for "keyboard"; see doMessages() + "key": MessagesPDP10.KEYS, // using "key" instead of "keys", since the latter is a method on JavasScript objects + "paper": MessagesPDP10.PAPER, + "read": MessagesPDP10.READ, + "write": MessagesPDP10.WRITE, + "serial": MessagesPDP10.SERIAL, + "timer": MessagesPDP10.TIMER, + "speaker": MessagesPDP10.SPEAKER, + "computer": MessagesPDP10.COMPUTER, + "log": MessagesPDP10.LOG, + "warn": MessagesPDP10.WARN, + /* + * Now we turn to message actions rather than message types; for example, setting "halt" + * on or off doesn't enable "halt" messages, but rather halts the CPU on any message above. + * + * Similarly, "m buffer on" turns on message buffering, deferring the display of all messages + * until "m buffer off" is issued. + */ + "buffer": MessagesPDP10.BUFFER, + "halt": MessagesPDP10.HALT +}; + +if (NODE) module.exports = MessagesPDP10; diff --git a/modules/pdp10/lib/nodebugger.js b/modules/pdp10/lib/nodebugger.js new file mode 100644 index 000000000..e4c83aa08 --- /dev/null +++ b/modules/pdp10/lib/nodebugger.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Compile-time definitions for Debugger-less configurations. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +/* + * WARNING: DEBUGGER needs to accurately reflect whether or not the Debugger component is (or will be) loaded. + * In the compiled case, we rely on the Closure Compiler to override DEBUGGER as appropriate. When it's *false*, + * nearly all of debugger.js will be conditionally removed by the compiler, reducing it to little more than a + * "type skeleton", which also solves some type-related warnings we would otherwise have if we tried to remove + * debugger.js from the compilation process altogether. + * + * However, when we're in "development mode" and running uncompiled code in debugger-less configurations, + * I would still like to skip loading debugger.js altogether. To do that, we must arrange for this additional file, + * nodebugger.js, to be loaded immediately after defines.js, *explicitly* overriding the previously defined value + * of DEBUGGER with *false*. + */ +DEBUGGER = false; diff --git a/modules/pdp10/lib/panel.js b/modules/pdp10/lib/panel.js new file mode 100644 index 000000000..538c70c75 --- /dev/null +++ b/modules/pdp10/lib/panel.js @@ -0,0 +1,1266 @@ +/** + * @fileoverview Implements the PDP-10 Panel component. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +if (NODE) { + var Str = require("../../shared/lib/strlib"); + var Web = require("../../shared/lib/weblib"); + var Component = require("../../shared/lib/component"); + var State = require("../../shared/lib/state"); + var PDP10 = require("./defines"); + var BusPDP10 = require("./bus"); + var MessagesPDP10 = require("./messages"); +} + +/** + * Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default, + * it deters us from defining named properties on our components; eg: + * + * this['exports'] = {...} + * + * results in an error: + * + * Cannot do '[]' access on a struct + * + * So, in order to define 'exports', we must override the @struct assumption by annotating + * the class as @unrestricted (or @dict). Note that this must be done both here and in the + * Component class, because otherwise the Compiler won't allow us to *reference* the named + * property either. + * + * TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to + * define every single property the class uses in its constructor, which results in a fair + * bit of redundant initialization, since many properties aren't (and don't need to be) fully + * initialized until the appropriate init(), reset(), restore(), etc. function is called. + * + * The upside, however, may be that since the structure of the class is completely defined by + * the constructor, JavaScript engines may be able to optimize and run more efficiently. + * + * @unrestricted + */ +class PanelPDP10 extends Component { + /** + * PanelPDP10(parmsPanel) + * + * The PanelPDP10 component has no required (parmsPanel) properties. + * + * @param {Object} parmsPanel + * @param {boolean} fBindings (true if panel may have bindings, otherwise not) + */ + constructor(parmsPanel, fBindings) + { + super("Panel", parmsPanel, MessagesPDP10.PANEL); + + /* + * If there are any live registers, LEDs, etc, to display, this will provide a count. + * TODO: Add some UI for fDisplayLiveRegs (either an XML property, or a UI checkbox, or both). + */ + this.cLiveRegs = 0; + this.nDisplayCount = 0; + this.nDisplayLimit = 60; + this.fDisplayLiveRegs = true; + this.fBindings = fBindings; + + /* + * regSwitches contains the Front Panel (aka Console) SWITCH register, which is also available + * as a read-only register at 177570 (but only the low 16 bits). regDisplay contains the DISPLAY + * register, a write-only register at the same address. + * + * regAddr is an internal register containing the contents of the Front Panel's ADDRESS display, + * and regData corresponds to the DATA display. They are updated by updateAddr() and updateData(), + * which in turn take care of calling updateLEDArray(). + * + * The state of ALL switches is maintained in this.switches, and likewise all LED states are + * maintained in this.leds, but for convenience, we also mirror some of those states in dedicated + * variables (eg, regSwitches for the SWITCH register, fLEDTest for the 'TEST' switch, etc). + */ + this.regDisplay = 0; + this.regSwitches = 0; + this.regAddr = this.regData = 0; + this.ledAddr = this.ledData = -1; + + /* + * The panel hardware has the following additional (supported) state; note that there are several + * settings on a real Front Panel that we don't support (eg, stepping one cycle vs. one instruction). + * + * While my initial intent is to eventually support all the ADDRSEL switch settings, I probably + * won't bother with any DATASEL switch settings; instead, I will automatically display the DISPLAY + * register (regDisplay) [the equivalent of selecting 'DISPLAY REGISTER'] except when data is being + * examined or deposited [the equivalent of selecting 'DATA PATHS']. + */ + this.fLEDTest = false; // LED (lamp) test in progress + this.fExamine = false; // true if the previously pressed switch was the 'EXAM' switch + this.fDeposit = false; // true if the previously pressed switch was the 'DEP' switch + this.nAddrSel = PanelPDP10.ADDRSEL.CONS_PHY; + + /* + * Every LED has a simple numeric value, assigned when setBinding() is called: + * + * zero if "off", non-zero if "on" + * + * initBus() will call displayLEDs() to ensure that every LED is set to its initial value. + */ + this.leds = {}; + + /* + * Every switch has an array associated with it: + * + * [0]: initial value of switch (0 if "down", 1 if "up") + * [1]: current value of switch + * [2]: true if the switch is momentary, false if not + * [3]: true if the switch is currently pressed, false if released + * [4]: optional handler to call whenever the switch is pressed or released + * [5]: optional switch index (used with CNSW switches 'S0' through 'S21') + * + * initBus() will call displaySwitches() to ensure that every switch is the position represented below. + * + * NOTE: Not all switches have the same "process" criteria. For example, 'TEST' will perform a LED test + * when it is momentarily pressed "up", whereas 'LOAD [ADRS]' will load the ADDRESS register from the + * SWITCH register when it is momentarily pressed "down". + * + * This means that processLEDTest(value) must act when value == 1 ("up"), whereas processLoadAddr(value) + * must act when value == 0 ("down"). You can infer all this from the table below, because the initial value + * of any momentary switch is its "inactive" value, so the opposite is its "active" value. + */ + this.switches = { + 'START': [1, 1, true, false, this.processStart], + 'STEP': [1, 1, false, false, this.processStep], + 'ENABLE': [1, 1, false, false, this.processEnable], + 'CONT': [1, 1, true, false, this.processContinue], + 'DEP': [0, 0, true, false, this.processDeposit], + 'EXAM': [1, 1, true, false, this.processExamine], + 'LOAD': [1, 1, true, false, this.processLoadAddr], + 'TEST': [0, 0, true, false, this.processLEDTest] + }; + for (var i = 0; i < 22; i++) { + this.switches['S'+i] = [0, 0, false, false, this.processSRSwitch, i]; + } + + /** @type {ComputerPDP10} */ + this.cmp = null; + + /** @type {BusPDP10} */ + this.bus = null; + + /** @type {CPUStatePDP10} */ + this.cpu = null; + + /** @type {DebuggerPDP10} */ + this.dbg = null; + + /* + * The 'hold' and 'toggle' exports, which map to holdSwitch() and toggleSwitch(), both press and release + * the specified switch, but processCommands() considers a 'hold' function to be asynchronous, which means + * that holdSwitch() will be passed a callback function that can be used to implement a delay between the + * press and the release, whereas toggleSwitch() will not. + * + * holdSwitch() only makes sense for momentary switches (eg, 'TEST'), where a visual delay might be nice. + * If the switch isn't momentary, or no delay is desired, then use toggleSwitch(); it will be more efficient. + * + * Finally, for switches that are toggles (eg, 'ENABLE'), you can use setSwitch() to set it to a specific + * state: zero for "off" and non-zero for "on". setSwitch() also supports meta-switches like "SR", using + * the entire value to set a series of switches at once; the value is assumed to be octal unless overridden + * by a prefix (eg, "0x") or suffix (eg, "."). + */ + this['exports'] = { + 'hold': this.holdSwitch, + 'toggle': this.toggleSwitch, + 'reset': this.resetSwitches, + 'set': this.setSwitch + }; + + this.setReady(); + } + + /** + * getAR() + * + * @this {PanelPDP10} + * @return {number} (current ADDRESS register) + */ + getAR() + { + return this.regAddr; + } + + /** + * setAR(value) + * + * @this {PanelPDP10} + * @param {number} value (new ADDRESS register) + */ + setAR(value) + { + this.updateAddr(this.regAddr = value); + } + + /** + * getDR() + * + * @this {PanelPDP10} + * @return {number} (current DISPLAY register) + */ + getDR() + { + return this.regDisplay; + } + + /** + * setDR(value) + * + * @this {PanelPDP10} + * @param {number} value (new DISPLAY register) + * @return {number} + */ + setDR(value) + { + return this.updateData(this.regDisplay = value); + } + + /** + * getSR() + * + * @this {PanelPDP10} + * @return {number} (current SWITCH register) + */ + getSR() + { + return this.regSwitches; + } + + /** + * setSR(value) + * + * @this {PanelPDP10} + * @param {number} value (new SWITCH register) + */ + setSR(value) + { + this.setSRSwitches(value); + } + + /** + * getSwitch(name) + * + * @this {PanelPDP10} + * @param {string} name + * @return {number|undefined} 0 if switch is off ("down"), 1 if on ("up"), or undefined if unrecognized + */ + getSwitch(name) + { + return this.switches[name] && this.switches[name][1]; + } + + /** + * reset(fPowerUp) + * + * NOTE: Since we've registered our handler with the Bus component, we will be called twice whenever + * the entire machine is reset: once when the Computer's reset() handler calls the Bus's reset() handler, + * and again when the Computer's reset() handler calls us directly. Multiple resets should be harmless. + * + * @this {PanelPDP10} + * @param {boolean} [fPowerUp] + */ + reset(fPowerUp) + { + /* + * Simulate a call to our stop() handler, to update the panel's ADDRESS register with the current PC. + */ + this.stop(); + if (fPowerUp) this.setDR(0); + } + + /** + * setBinding(sType, sBinding, control, sValue) + * + * Some panel layouts don't have bindings of their own, and even when they do, there may still be some + * components (eg, the CPU) that prefer to update their own bindings, so we pass along all binding requests + * to the Computer, CPU, Keyboard and Debugger components first. The order shouldn't matter, since any + * component that doesn't recognize the specified binding should simply ignore it. + * + * @this {PanelPDP10} + * @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc) + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sType, sBinding, control, sValue) + { + if (this.cmp && this.cmp.setBinding(sType, sBinding, control, sValue)) { + return true; + } + if (this.cpu && this.cpu.setBinding(sType, sBinding, control, sValue)) { + return true; + } + if (DEBUGGER && this.dbg && this.dbg.setBinding(sType, sBinding, control, sValue)) { + return true; + } + + switch (sBinding) { + case 'R0': + case 'R1': + case 'R2': + case 'R3': + case 'R4': + case 'R5': + case 'R6': + case 'R7': + case 'NF': + case 'ZF': + case 'VF': + case 'CF': + case 'PS': + this.bindings[sBinding] = control; + this.cLiveRegs++; + return true; + + default: + /* + * Square ("led") or round ("rled") LEDs are defined in machine XML files like so: + * + * + * + * Only *type* and *binding* attributes are required; if *value* is omitted, the default value is 0 ("off"). + */ + if (sType == "led" || sType == "rled") { + this.bindings[sBinding] = control; + this.leds[sBinding] = sValue? 1 : 0; + this.cLiveRegs++; + return true; + } + /* + * Switches are defined in machine XML files like so: + * + * + * + * Only *type* and *binding* attributes are required; if *value* is omitted, the default value is 0 ("down"). + * + * Currently, there is no XML attribute to indicate whether a switch is "momentary"; only recognized switches + * in our internal table can have that attribute. + */ + if (sType == "switch") { + /* + * Like LEDs, we allow unrecognized switches to be defined as well, but they won't do anything useful, + * since only recognized switches will have handlers that perform the appropriate operations. + */ + if (this.switches[sBinding] === undefined) { + this.switches[sBinding] = [sValue? 1 : 0, sValue? 1 : 0]; + } + this.bindings[sBinding] = control; + var parent = control.parentElement || control; + parent = parent.parentElement || parent; + parent.onmousedown = function(panel, sBinding) { + return function onPressSwitch() { + panel.pressSwitch(sBinding); + }; + }(this, sBinding); + parent.onmouseup = parent.onmouseout = function(panel, sBinding) { + return function onReleaseSwitch() { + panel.releaseSwitch(sBinding); + }; + }(this, sBinding); + parent.ontouchstart = function(panel, sBinding) { + return function onPressSwitch(event) { + panel.pressSwitch(sBinding); + event.preventDefault(); + }; + }(this, sBinding); + parent.ontouchend = function(panel, sBinding) { + return function onReleaseSwitch() { + panel.releaseSwitch(sBinding); + }; + }(this, sBinding); + return true; + } + return super.setBinding(sType, sBinding, control, sValue); + } + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * @this {PanelPDP10} + * @param {ComputerPDP10} cmp + * @param {BusPDP10} bus + * @param {CPUStatePDP10} cpu + * @param {DebuggerPDP10} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.cmp = cmp; + this.bus = bus; + this.cpu = cpu; + this.dbg = dbg; + + this.displayLEDs(); + this.displaySwitches(); + } + + /** + * powerUp(data, fRepower) + * + * @this {PanelPDP10} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (!fRepower) { + /* + * As noted in init(), our powerUp() method gives us a second opportunity to notify any + * components that that might care (eg, CPU, Keyboard, and Debugger) that we have some controls + * (ie, bindings) they might want to use. + */ + if (this.fBindings) PanelPDP10.init(); + + if (!data) { + this.reset(true); + } else { + if (!this.restore(data)) return false; + } + } + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * @this {PanelPDP10} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + return fSave? this.save() : true; + } + + /** + * save() + * + * This implements save support for the PanelPDP10 component. + * + * @this {PanelPDP10} + * @return {Object} + */ + save() + { + var state = new State(this); + state.set(0, [ + this.getAR(), + this.getDR(), + this.getSR() + ]); + return state.data(); + } + + /** + * restore(data) + * + * This implements restore support for the PanelPDP10 component. + * + * @this {PanelPDP10} + * @param {Object} data + * @return {boolean} true if successful, false if failure + */ + restore(data) + { + var a = data[0]; + if (a) { + this.setAR(a[0]); + this.setDR(a[1]); + this.setSR(a[2]); + } + return true; + } + + /** + * resetSwitches() + * + * @this {PanelPDP10} + * @return {boolean} + */ + resetSwitches() + { + for (var sBinding in this.switches) { + var sw = this.switches[sBinding]; + sw[1] = sw[0]; + } + this.displaySwitches(); + return true; + } + + /** + * displayLED(sBinding, value) + * + * @this {PanelPDP10} + * @param {string} sBinding + * @param {boolean|number} value (true or non-zero if the LED should be on, false or zero if off) + */ + displayLED(sBinding, value) + { + var control = this.bindings[sBinding]; + if (control) { + /* + * TODO: Add support for user-definable LED colors? + */ + control.style.backgroundColor = (value? "#ff0000" : "#000000"); + } + } + + /** + * displayLEDs(override) + * + * @this {PanelPDP10} + * @param {boolean|number|null} [override] (true turn on all LEDs, false to turn off all LEDs, null or undefined for normal LED activity) + */ + displayLEDs(override) + { + for (var sBinding in this.leds) { + this.displayLED(sBinding, override != null? override : this.leds[sBinding]); + } + } + + /** + * displaySwitch(sBinding, value) + * + * @this {PanelPDP10} + * @param {string} sBinding + * @param {boolean|number} value (true if the switch should be "up" (on), false if "down" (off)) + */ + displaySwitch(sBinding, value) + { + var control = this.bindings[sBinding]; + if (control) { + control.style.marginTop = (value? "0px" : "20px"); + control.style.backgroundColor = (value? "#00ff00" : "#228B22"); + } + } + + /** + * displaySwitches() + * + * @this {PanelPDP10} + */ + displaySwitches() + { + for (var sBinding in this.switches) { + this.displaySwitch(sBinding, this.switches[sBinding][1]); + } + } + + /** + * displayValue(sLabel, nValue, cch) + * + * This is principally for displaying register values, but in reality, it can be used to display any + * numeric value bound to the given label. + * + * @this {PanelPDP10} + * @param {string} sLabel + * @param {number} nValue + * @param {number} [cch] + */ + displayValue(sLabel, nValue, cch) + { + if (this.bindings[sLabel]) { + var sVal; + var nBase = this.dbg && this.dbg.nBase || 8; + nValue = nValue || 0; + if (!this.cpu.isRunning() || this.fDisplayLiveRegs) { + sVal = nBase == 8? Str.toOct(nValue, cch) : Str.toHex(nValue, cch); + } else { + sVal = "--------".substr(0, cch || 4); + } + /* + * TODO: Determine if this test actually avoids any redrawing when a register hasn't changed, and/or if + * we should maintain our own (numeric) cache of displayed register values (to avoid creating these temporary + * string values that will have to garbage-collected), and/or if this is actually slower, and/or if I'm being + * too obsessive. + */ + if (this.bindings[sLabel].textContent != sVal) this.bindings[sLabel].textContent = sVal; + } + } + + /** + * holdSwitch(fnCallback, sBinding, sDelay) + * + * @this {PanelPDP10} + * @param {function()|null} fnCallback + * @param {string} sBinding + * @param {string} [sDelay] + * @return {boolean} false if wait required, true otherwise + */ + holdSwitch(fnCallback, sBinding, sDelay) + { + if (this.pressSwitch(sBinding)) { + if (sDelay) { + var panel = this; + setTimeout(function() { + panel.releaseSwitch(sBinding); + if (fnCallback) fnCallback(); + }, +sDelay); + return false; + } else { + this.releaseSwitch(sBinding); + } + } + return true; + } + + /** + * setSwitch(sBinding, sValue) + * + * @this {PanelPDP10} + * @param {string} sBinding + * @param {string} sValue + * @return {boolean} + */ + setSwitch(sBinding, sValue) + { + if (sBinding == "SR") { + return this.setSRSwitches(Str.parseInt(sValue, 8)) + } + var sw = this.switches[sBinding]; + if (sw) { + sw[1] = +sValue? 1 : 0; + this.displaySwitch(sBinding, sw[1]); + return true; + } + return false; + } + + /** + * toggleSwitch(sBinding) + * + * @this {PanelPDP10} + * @param {string} sBinding + * @return {boolean} + */ + toggleSwitch(sBinding) + { + if (this.pressSwitch(sBinding)) { + this.releaseSwitch(sBinding); + return true; + } + return false; + } + + /** + * pressSwitch(sBinding) + * + * @this {PanelPDP10} + * @param {string} sBinding + * @return {boolean} + */ + pressSwitch(sBinding) + { + var sw = this.switches[sBinding]; + if (sw) { + /* + * Set the new switch value in sw[1] and then immediately display it + */ + this.displaySwitch(sBinding, (sw[1] = 1 - sw[1])); + + /* + * Mark the switch as "pressed" + */ + sw[3] = true; + + /* + * Call the appropriate process handler with the current switch value (sw[1]) + */ + if (sw[4]) sw[4].call(this, sw[1], sw[5]); + + /* + * This helps the next 'DEP' or 'EXAM' press determine if the previous press was the same, + * while also ignoring any intervening 'STEP' presses (see processStep() for why we do that). + */ + if (sBinding != PanelPDP10.SWITCH.STEP) { + this.fDeposit = (sBinding == PanelPDP10.SWITCH.DEP); + this.fExamine = (sBinding == PanelPDP10.SWITCH.EXAM); + } + return true; + } + return false; + } + + /** + * releaseSwitch(sBinding) + * + * @this {PanelPDP10} + * @param {string} sBinding + * @return {boolean} + */ + releaseSwitch(sBinding) + { + /* + * pressSwitch() is simple: flip the switch's current value in sw[1] and marked it "pressed" in sw[3]. + * + * releaseSwitch() is more complicated, because we must handle both mouseUp and mouseOut events. The first time + * we receive EITHER of those events AND the switch is marked momentary (sw[2]) AND the switch is pressed (sw[3]), + * then we must flip the switch back to its original value. + * + * Otherwise, the only thing we have to do is mark the switch as "released" (ie, set sw[3] to false). + */ + var sw = this.switches[sBinding]; + if (sw) { + if (sw[2] && sw[3]) { + /* + * Set the new switch value in sw[1] and then immediately display it + */ + this.displaySwitch(sBinding, (sw[1] = sw[0])); + + /* + * Call the appropriate process handler with the current switch value (sw[1]) + */ + if (sw[4]) sw[4].call(this, sw[1], sw[5]); + } + /* + * Mark the switch as "released" + */ + sw[3] = false; + return true; + } + return false; + } + + /** + * processStart(value, index) + * + * @this {PanelPDP10} + * @param {number} value + * @param {number} [index] + */ + processStart(value, index) + { + if (!value && !this.cpu.isRunning()) { + + this.cpu.setPC(this.regAddr); + + /* + * The PDP-11/70 Handbook goes on to say: "If the system needs to be initialized but execution + * is not wanted, the START switch should be depressed while the HALT/ENABLE switch is in the HALT + * position." + */ + if (this.getSwitch(PanelPDP10.SWITCH.ENABLE)) { + this.cpu.startCPU(); + } + } + } + + /** + * processStep(value, index) + * + * If value == 1 (our initial value), then the 'STEP' switch is set to "S INST" (step one instruction); + * otherwise, it's set to "S BUS CYCLE" (step one bus cycle). + * + * However, since we can't currently support cycle-stepping, I've decided to innovate a little and + * change the meaning of this switch: the normal ("up") position means that successive 'EXAM' and 'DEP' + * operations will first add 2 to the ADDRESS register, while the opposite ("down") position means + * they will first subtract 2. + * + * See processLEDTest() for more of these exciting "innovations". ;-) + * + * @this {PanelPDP10} + * @param {number} value + * @param {number} [index] + */ + processStep(value, index) + { + /* + * There's really nothing for us to do here, because the normal press and release handlers + * already record the state of this switch, so it can be queried as needed, using getSwitch(). + */ + } + + /** + * processEnable(value, index) + * + * If value == 1 (our initial value), then the 'ENABLE'/'HALT' switch is set to 'ENABLE', otherwise 'HALT'. + * + * @this {PanelPDP10} + * @param {number} value + * @param {number} [index] + */ + processEnable(value, index) + { + /* + * The "down" (0) position is 'HALT', which stops the CPU; however, the "up" (1) position ('ENABLE') + * does NOT start the CPU. You must press 'CONT' to continue execution, which will either continue for + * one instruction if this switch to set to 'HALT' or indefinitely if it is set to 'ENABLE'. + */ + if (!value) { + this.cpu.stopCPU(); + } + } + + /** + * processContinue(value, index) + * + * @this {PanelPDP10} + * @param {number} value + * @param {number} [index] + */ + processContinue(value, index) + { + if (!value && !this.cpu.isRunning()) { + /* + * TODO: Technically, we're also supposed to check the 'STEP' switch to determine if we should + * step one instruction or just one cycle, but we don't currently have the ability to do the latter. + */ + if (!this.getSwitch(PanelPDP10.SWITCH.ENABLE)) { + /* + * Using the Debugger's stepCPU() function is more convenient, and has the pleasant side-effect + * of updating the debugger's display; however, not all machines with a Front Panel will necessarily + * also have the Debugger loaded. + */ + var dbg = this.dbg; + if (dbg && !dbg.isBusy(true)) { + dbg.setBusy(true); + dbg.stepCPU(0, null); + dbg.setBusy(false); + } + else { + /* + * For this tiny single-instruction burst, mimic what runCPU() does. + */ + try { + var nCyclesStep = this.cpu.stepCPU(1); + if (nCyclesStep > 0) { + this.cpu.updateTimers(nCyclesStep); + this.cpu.addCycles(nCyclesStep, true); + this.cpu.updateChecksum(nCyclesStep); + } + } + catch(exception) { + /* + * We assume that any numeric exception was explicitly thrown by the CPU to interrupt the + * current instruction. For all other exceptions, we attempt a stack dump. + */ + if (typeof exception != "number") { + var e = exception; + this.cpu.setError(e.stack || e.message); + } + } + } + + /* + * Simulate a call to our stop() handler, to update the panel's ADDRESS register with the new PC. + */ + this.stop(); + + /* + * Going through the normal channels (ie, the Computer's updateDisplays() interface) ensures that + * ALL updateDisplay() handlers will be called, including ours. + * + * NOTE: If we used the Debugger's stepCPU() function, then that includes a call to updateDisplay(); + * unfortunately, it will have happened BEFORE we called stop() to update the ADDRESS register, so + * we still need to call it again. + */ + if (this.cmp) this.cmp.updateDisplays(); + } + else { + this.cpu.startCPU(); + } + } + } + + /** + * processDeposit(value, index) + * + * @this {PanelPDP10} + * @param {number} value + * @param {number} [index] + */ + processDeposit(value, index) + { + if (value && !this.cpu.isRunning()) { + if (this.fDeposit) this.advanceAddr(); + /* + * This used to be updateData(), but that only updates regData, whereas setDR() updates both regData and regDisplay, + * and for these kinds of explicit Front Panel operations, I'm assuming the values should be synced. + */ + var w = this.setDR(this.regSwitches); + + if (this.nAddrSel == PanelPDP10.ADDRSEL.CONS_PHY) { + /* + * TODO: Determine if this needs to take the UNIBUS map into consideration. + */ + this.bus.setWordDirect(this.regAddr, w); + } else { + /* + * TODO: This code is obviously incomplete, since it doesn't take into account the precise ADDRSEL mode. + */ + this.cpu.writeWord(this.regAddr, w); + } + } + } + + /** + * processExamine(value, index) + * + * @this {PanelPDP10} + * @param {number} value + * @param {number} [index] + */ + processExamine(value, index) + { + if (!value && !this.cpu.isRunning()) { + var w; + if (this.fExamine) this.advanceAddr(); + if (this.nAddrSel == PanelPDP10.ADDRSEL.CONS_PHY) { + /* + * TODO: Determine if this needs to take the UNIBUS map into consideration. + */ + w = this.bus.getWordDirect(this.regAddr); + } else { + /* + * TODO: This code is obviously incomplete, since it doesn't take into account the precise ADDRSEL mode. + */ + w = this.cpu.readWord(this.regAddr); + } + /* + * This used to be updateData(), but that only updates regData, whereas setDR() updates both regData and regDisplay, + * and for these kinds of explicit Front Panel operations, I'm assuming the values should be synced. + */ + this.setDR(w); + } + } + + /** + * processLoadAddr(value, index) + * + * @this {PanelPDP10} + * @param {number} value + * @param {number} [index] + */ + processLoadAddr(value, index) + { + if (!value && !this.cpu.isRunning()) { + this.updateAddr(this.regSwitches); + } + } + + /** + * processLEDTest(value, index) + * + * @this {PanelPDP10} + * @param {number} value + * @param {number} [index] + */ + processLEDTest(value, index) + { + if (value) { + this.fLEDTest = true; + this.displayLEDs(true); + } else { + this.fLEDTest = false; + this.displayLEDs(); + /* + * This is another one of my "innovations": when you're done testing the LEDs, all the switches reset as well. + */ + this.setSRSwitches(0); + } + } + + /** + * processSRSwitch(value, index) + * + * @this {PanelPDP10} + * @param {number} value (normally 0 or 1, but we only depend on it being zero or non-zero) + * @param {number} index + */ + processSRSwitch(value, index) + { + if (value) { + this.regSwitches |= 1 << index; + } else { + this.regSwitches &= ~(1 << index); + } + } + + /** + * advanceAddr() + * + * This should also take care of the following Front Panel behaviors when the accessing the general-purpose + * registers: + * + * 1) ADDRESS display incremented by 1 (instead of 2) + * 2) The STEP after the last register is 177700, such that the addresses are looped + * + * A third behavior is NOT emulated: preventing the ADDRESS from stepping to the first General Register (177700) + * from 177676. + * + * @this {PanelPDP10} + * @return {number} + */ + advanceAddr() + { + var inc = 1; + var mask = this.bus.nBusMask; + if (!this.getSwitch(PanelPDP10.SWITCH.STEP)) inc = -inc; + return this.updateAddr((this.regAddr & ~mask) | ((this.regAddr + inc) & mask)); + } + + /** + * updateAddr(value) + * + * @this {PanelPDP10} + * @param {number} value + * @return {number} + */ + updateAddr(value) + { + this.regAddr = value & this.bus.nBusMask; + if (this.ledAddr !== this.regAddr) { + this.ledAddr = this.regAddr; + this.updateLEDArray("A", this.ledAddr, 22); + } + return this.regAddr; + } + + /** + * updateData(value) + * + * @this {PanelPDP10} + * @param {number} value + * @return {number} + */ + updateData(value) + { + this.regData = value % PDP10.DATA_LIMIT; + if (this.ledData !== this.regData) { + this.ledData = this.regData; + this.updateLEDArray("D", this.ledData, 16); + } + return this.regData; + } + + /** + * updateLED(sBinding, value) + * + * @this {PanelPDP10} + * @param {string} sBinding + * @param {number} value + * @return {number} + */ + updateLED(sBinding, value) + { + this.leds[sBinding] = value; + if (!this.fLEDTest) this.displayLED(sBinding, value); + return value; + } + + /** + * updateLEDArray(sPrefix, value, nLEDs) + * + * @this {PanelPDP10} + * @param {string} sPrefix + * @param {number} value + * @param {number} nLEDs + */ + updateLEDArray(sPrefix, value, nLEDs) + { + for (var i = 0; i < nLEDs; i++) { + var sBinding = sPrefix + i; + this.updateLED(sBinding, value & (1 << i)); + } + } + + /** + * setSRSwitches(value) + * + * @this {PanelPDP10} + * @param {number|undefined} value + * @return {boolean} + */ + setSRSwitches(value) + { + this.regSwitches = value | 0; + for (var i = 0; i < 22; i++) { + this.switches['S'+i][1] = (this.regSwitches & (1 << i))? 1 : 0; + } + /* + * This (re)displays ALL switches, not merely the SR switches, but that's OK. + */ + this.displaySwitches(); + return true; + } + + /** + * stop(ms, nCycles) + * + * This is a notification handler, called by the Computer, to inform us the CPU has now stopped. + * + * @this {PanelPDP10} + * @param {number} [ms] + * @param {number} [nCycles] + */ + stop(ms, nCycles) + { + this.updateAddr(this.cpu.getPC()); + } + + /** + * setAddr(value, fActive) + * + * This interface is for passing new addresses to the Front Panel. However, whether or not this will become the + * ADDRESS actually displayed will depend on other settings (see updateStatus() for details). + * + * @this {PanelPDP10} + * @param {number} value + * @param {boolean} [fActive] (true if this should become the "active" ADDRESS regardless of other settings) + */ + setAddr(value, fActive) + { + this.regAddr = value; + } + + /** + * setData(value, fActive) + * + * This interface is for passing new data to the Front Panel. However, whether or not this will become the + * DATA actually displayed will depend on the Front Panel's DATASEL switch setting, as well as the fActive flag. + * + * @this {PanelPDP10} + * @param {number} value + * @param {boolean} [fActive] (true if this should become the "active" DATA regardless of the DATASEL switch setting) + */ + setData(value, fActive) + { + if (!fActive) { + this.regData = value; + } else { + this.regDisplay = value; + } + } + + /** + * updateDisplay(nUpdate) + * + * Called by the Computer component at intervals to update registers, LEDs, etc. + * + * @this {PanelPDP10} + * @param {number} [nUpdate] (-2 for power on, -1 for forced, > 0 for periodic, 0 or undefined otherwise) + */ + updateDisplay(nUpdate) + { + if (this.cLiveRegs) { + + var fRunning = this.cpu.isRunning(); + var fWaiting = this.cpu.isWaiting(); + + if (nUpdate < 0 || !fRunning || this.fDisplayLiveRegs) { + + /* + * We arbitrarily separate the display elements into two categories: cheap and expensive. + * + * LEDs are considered cheap, register displays are not. So we'll skip the latter if this + * is a periodic update AND our periodic update counter hasn't reached the periodic update limit. + */ + if (nUpdate <= 0 || (this.nDisplayCount += nUpdate) >= this.nDisplayLimit) { + this.displayValue("PC", this.cpu.getPC()); + this.nDisplayCount = 0; + } + + /* + * Update the ADDRESS and DATA LEDs by selecting the appropriate values. + * + * TODO: There is currently no mechanism for selecting regData over regDisplay; + * we are acting as if the DATASEL switch setting is locked to "DISPLAY REGISTER". + */ + if (nUpdate < -1) { + this.regAddr = this.cpu.getPC(); + } else if (nUpdate > 0 && fRunning && !fWaiting) { + this.regAddr = this.cpu.getLastAddr(); + } + + this.updateAddr(this.regAddr); + this.updateData(this.regDisplay); + } + } + } + + /** + * PanelPDP10.init() + * + * This function operates on every HTML element of class "panel", extracting the + * JSON-encoded parameters for the PanelPDP10 constructor from the element's "data-value" + * attribute, invoking the constructor to create a PanelPDP10 component, and then binding + * any associated HTML controls to the new component. + * + * NOTE: Unlike most other component init() functions, this one is designed to be + * called multiple times: once at load time, so that we can bind our print() + * function to the panel's output control ASAP, and again when the Computer component + * is verifying that all components are ready and invoking their powerUp() functions. + * + * Our powerUp() method gives us a second opportunity to notify any components that + * that might care (eg, CPU, Keyboard, and Debugger) that we have some controls they + * might want to use. + */ + static init() + { + var aePanels = Component.getElementsByClass(document, PDP10.APPCLASS, "panel"); + for (var iPanel=0; iPanel < aePanels.length; iPanel++) { + var ePanel = aePanels[iPanel]; + var parmsPanel = Component.getComponentParms(ePanel); + var panel = Component.getComponentByID(parmsPanel['id']); + if (!panel) panel = new PanelPDP10(parmsPanel, true); + Component.bindComponentControls(panel, ePanel, PDP10.APPCLASS); + } + } +} + +PanelPDP10.ADDRSEL = { + CONS_PHY: 7 // use a physical address to perform console operations (e.g., LOAD ADRS, EXAM, & DEP) +}; + +/* + * To get the current state of a switch; eg:: + * + * this.getSwitch(PanelPDP10.SWITCH.ENABLE) + * + * I haven't filled out this table, primarily it only needs to list switches we actually query + * (eg, non-momentary ones like 'ENABLE' and 'STEP', and 'EXAM' and 'DEP' since they have special + * "step" behavior when pressed more than once in a row). Ditto for the LED table. + */ +PanelPDP10.SWITCH = { + DEP: 'DEP', + ENABLE: 'ENABLE', + EXAM: 'EXAM', + STEP: 'STEP' +}; + +/* + * Initialize every Panel module on the page. + */ +Web.onInit(PanelPDP10.init); + +if (NODE) module.exports = PanelPDP10; diff --git a/modules/pdp10/lib/ram.js b/modules/pdp10/lib/ram.js new file mode 100644 index 000000000..226e9e0a3 --- /dev/null +++ b/modules/pdp10/lib/ram.js @@ -0,0 +1,413 @@ +/** + * @fileoverview Implements the PDP-10 RAM component. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +if (NODE) { + var Str = require("../../shared/lib/strlib"); + var Web = require("../../shared/lib/weblib"); + var DumpAPI = require("../../shared/lib/dumpapi"); + var Component = require("../../shared/lib/component"); + var PDP10 = require("./defines"); + var MemoryPDP10 = require("./memory"); + var MessagesPDP10 = require("./messages"); +} + +class RAMPDP10 extends Component { + /** + * RAMPDP10(parmsRAM) + * + * The RAMPDP10 component expects the following (parmsRAM) properties: + * + * addr: starting physical address of RAM (default is 0) + * size: amount of RAM, in bytes (default is 0, which means defer to motherboard switch settings) + * file: name of optional data file to load into RAM (default is "") + * load: optional file load address (overrides any load address specified in the data file; default is null) + * exec: optional file exec address (overrides any exec address specified in the data file; default is null) + * + * NOTE: We make a note of the specified size, but no memory is initially allocated for the RAM until the + * Computer component calls powerUp(). + * + * @param {Object} parmsRAM + */ + constructor(parmsRAM) + { + super("RAM", parmsRAM); + + this.abInit = null; + this.aSymbols = null; + + this.addrRAM = +parmsRAM['addr']; + this.sizeRAM = +parmsRAM['size']; + + this.addrLoad = parmsRAM['load']; + this.addrExec = parmsRAM['exec']; + if (this.addrLoad != null) this.addrLoad = +this.addrLoad; + if (this.addrExec != null) this.addrExec = +this.addrExec; + + this.fInstalled = (!!this.sizeRAM); // 0 is the default value for 'size' when none is specified + this.fAllocated = false; + + this.sFilePath = parmsRAM['file']; + this.sFileName = Str.getBaseName(this.sFilePath); + + if (this.sFilePath) { + var sFileURL = this.sFilePath; + if (DEBUG) this.log('load("' + sFileURL + '")'); + /* + * If the selected data file has a ".json" extension, then we assume it's pre-converted + * JSON-encoded data, so we load it as-is; ditto for ROM files with a ".hex" extension. + * Otherwise, we ask our server-side converter to return the file in a JSON-compatible format. + */ + var sFileExt = Str.getExtension(this.sFileName); + if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) { + sFileURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true'; + } + var ram = this; + Web.getResource(sFileURL, null, true, function doneLoad(sURL, sResponse, nErrorCode) { + ram.finishLoad(sURL, sResponse, nErrorCode); + }); + } + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * @this {RAMPDP10} + * @param {ComputerPDP10} cmp + * @param {BusPDP10} bus + * @param {CPUStatePDP10} cpu + * @param {DebuggerPDP10} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.bus = bus; + this.cpu = cpu; + this.dbg = dbg; + this.initRAM(); + } + + /** + * powerUp(data, fRepower) + * + * @this {RAMPDP10} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (this.aSymbols) { + if (this.dbg) { + this.dbg.addSymbols(this.id, this.addrRAM, this.sizeRAM, this.aSymbols); + } + /* + * Our only role in the handling of symbols is to hand them off to the Debugger at our + * first opportunity. Now that we've done that, our copy of the symbols, if any, are toast. + */ + delete this.aSymbols; + } + if (!fRepower) { + /* + * Since we use the Bus to allocate all our memory, memory contents are already restored for us, + * so we don't save any state, and therefore no state should be restored. Just do a reset(). + */ + this.assert(!data); + this.reset(); + } + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * @this {RAMPDP10} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + /* + * The Computer powers down the CPU first, at which point CPUState state is saved, + * which includes the Bus state, and since we use the Bus component to allocate all + * our memory, memory contents are already saved for us, so we don't need the usual + * save logic. + */ + return true; + } + + /** + * finishLoad(sURL, sData, nErrorCode) + * + * @this {RAMPDP10} + * @param {string} sURL + * @param {string} sData + * @param {number} nErrorCode (response from server if anything other than 200) + */ + finishLoad(sURL, sData, nErrorCode) + { + if (nErrorCode) { + this.notice("Unable to load RAM resource (error " + nErrorCode + ": " + sURL + ")"); + this.sFilePath = null; + } + else { + Component.addMachineResource(this.idMachine, sURL, sData); + var resource = Web.parseMemoryResource(sURL, sData); + if (resource) { + this.abInit = resource.aBytes; + this.aSymbols = resource.aSymbols; + if (this.addrLoad == null) this.addrLoad = resource.addrLoad; + if (this.addrExec == null) this.addrExec = resource.addrExec; + } else { + this.sFilePath = null; + } + } + this.initRAM(); + } + + /** + * initRAM() + * + * This function is called by both initBus() and finishLoad(), but it cannot copy the initial data into place + * until after initBus() has received the Bus component AND finishLoad() has received the data. When both those + * criteria are satisfied, the component becomes "ready". + * + * @this {RAMPDP10} + */ + initRAM() + { + if (!this.bus) return; + + if (!this.fAllocated && this.sizeRAM) { + if (this.bus.addMemory(this.addrRAM, this.sizeRAM, MemoryPDP10.TYPE.RAM)) { + this.fAllocated = true; + } else { + this.sizeRAM = 0; // don't bother trying again (it just results in redundant error messages) + } + } + if (!this.isReady()) { + if (!this.fAllocated) { + Component.error("No RAM allocated"); + } + else if (this.sFilePath) { + /* + * Too early... + */ + if (!this.abInit || !this.bus) return; + + if (this.loadImage(this.abInit, this.addrLoad, this.addrExec, this.addrRAM)) { + this.status('Loaded image "' + this.sFileName + '"'); + } else { + this.notice('Error loading image "' + this.sFileName + '"'); + } + + /* + * NOTE: We now retain this data, so that reset() can return the RAM to its predefined state. + * + * delete this.abInit; + */ + } + this.setReady(); + } + } + + /** + * reset() + * + * @this {RAMPDP10} + */ + reset() + { + if (this.fAllocated) { + /* + * TODO: Add a configuration parameter for selecting the byte pattern on reset? + * Note that when memory blocks are originally created, they are currently always + * zero-initialized, so this would only affect resets. + */ + this.bus.zeroMemory(this.addrRAM, this.sizeRAM, 0); + if (this.abInit) { + this.loadImage(this.abInit, this.addrLoad, this.addrExec, this.addrRAM, !this.dbg); + } + } + } + + /** + * loadImage(aBytes, addrLoad, addrExec, addrInit, fStart) + * + * If the array contains a PAPER tape image in the "Absolute Format," load it as specified + * by the format; otherwise, load it as-is using the address(es) supplied. + * + * @this {RAMPDP10} + * @param {Array|Uint8Array} aBytes + * @param {number|null} [addrLoad] + * @param {number|null} [addrExec] (this CAN override any starting address INSIDE the image) + * @param {number|null} [addrInit] + * @param {boolean} [fStart] + * @return {boolean} (true if loaded, false if not) + */ + loadImage(aBytes, addrLoad, addrExec, addrInit, fStart) + { + var fStop = false; + var fLoaded = false; + /* + * Data on tapes in the "Absolute Format" is organized into blocks; each block begins with + * a 6-byte header: + * + * 2-byte signature (0x0001) + * 2-byte block length (N + 6, because it includes the 6-byte header) + * 2-byte load address + * + * followed by N data bytes. If N is zero, then the 2-byte load address is the exec address, + * unless the address is odd (usually 1). DEC's Absolute Loader jumps to the exec address + * in former case, halts in the latter. + * + * All values are stored "little endian" (low byte followed by high byte), just like the + * PDP-11's memory architecture. + * + * After the data bytes, there is a single checksum byte. The 8-bit sum of all the bytes in + * the block (including the header bytes and checksum byte) should be zero. + * + * ANOMALIES: Tape files don't always begin with a signature word, so I allow any number of + * leading zeros before the first signature. Tape files don't always end cleanly either, so as + * soon as I see an invalid signature, I break out of the loop without signalling an error, as + * long as at least ONE block was successfully processed. In fact, it's possible that as + * soon as a block with ZERO data bytes is encountered, processing is supposed to stop, but + * I haven't examined enough tapes (or the Absolute Loader code) to know for sure. + */ + if (addrLoad == null) { + var off = 0, fError = false; + while (off < aBytes.length - 1) { + var w = (aBytes[off] & 0xff) | ((aBytes[off+1] & 0xff) << 8); + if (!w) { // ignore pairs of leading zeros + off += 2; + continue; + } + if (!(w & 0xff)) { // as well as single bytes of zero + off++; + continue; + } + var offBlock = off; + if (w != 0x0001) { + this.printMessage("invalid signature (" + Str.toHexWord(w) + ") at offset " + Str.toHexWord(offBlock), MessagesPDP10.PAPER); + break; + } + if (off + 6 >= aBytes.length) { + this.printMessage("invalid block at offset " + Str.toHexWord(offBlock), MessagesPDP10.PAPER); + break; + } + off += 2; + var checksum = w; + var len = (aBytes[off++] & 0xff) | ((aBytes[off++] & 0xff) << 8); + var addr = (aBytes[off++] & 0xff) | ((aBytes[off++] & 0xff) << 8); + checksum += (len & 0xff) + (len >> 8) + (addr & 0xff) + (addr >> 8); + var offData = off, cbData = len -= 6; + while (len > 0 && off < aBytes.length) { + checksum += aBytes[off++] & 0xff; + len--; + } + if (len != 0 || off >= aBytes.length) { + this.printMessage("insufficient data for block at offset " + Str.toHexWord(offBlock), MessagesPDP10.PAPER); + break; + } + checksum += aBytes[off++] & 0xff; + if (checksum & 0xff) { + this.printMessage("invalid checksum (" + Str.toHexByte(checksum) + ") for block at offset " + Str.toHexWord(offBlock), MessagesPDP10.PAPER); + break; + } + if (!cbData) { + if (addr & 0x1) { + fStop = true; + } else { + if (addrExec == null) addrExec = addr; + } + if (addrExec != null) this.printMessage("starting address: " + Str.toHexWord(addrExec), MessagesPDP10.PAPER); + } else { + this.printMessage("loading " + Str.toHexWord(cbData) + " bytes at " + Str.toHexWord(addr) + "-" + Str.toHexWord(addr + cbData), MessagesPDP10.PAPER); + while (cbData--) { + this.bus.setWordDirect(addr++, aBytes[offData++]); + } + } + fLoaded = true; + } + } + if (!fLoaded) { + if (addrLoad == null) addrLoad = addrInit; + if (addrLoad != null) { + for (var i = 0; i < aBytes.length; i++) { + this.bus.setWordDirect(addrLoad + i, aBytes[i]); + } + fLoaded = true; + } + } + if (fLoaded) { + /* + * Set the start address to whatever the caller provided, or failing that, whatever start + * address was specified inside the image. + * + * For example, the diagnostic "MAINDEC-11-D0AA-PB" doesn't include a start address inside the + * image, but we know that the directions for that diagnostic say to "Start and Restart at 200", + * so we have manually inserted an "exec":128 in the JSON containing the image. + */ + if (addrExec == null || fStop) { + this.cpu.stopCPU(); + fStart = false; + } + if (addrExec != null) { + this.cpu.setReset(addrExec, fStart); + } + } + return fLoaded; + } + + /** + * RAMPDP10.init() + * + * This function operates on every HTML element of class "ram", extracting the + * JSON-encoded parameters for the RAMPDP10 constructor from the element's "data-value" + * attribute, invoking the constructor to create a RAMPDP10 component, and then binding + * any associated HTML controls to the new component. + */ + static init() + { + var aeRAM = Component.getElementsByClass(document, PDP10.APPCLASS, "ram"); + for (var iRAM = 0; iRAM < aeRAM.length; iRAM++) { + var eRAM = aeRAM[iRAM]; + var parmsRAM = Component.getComponentParms(eRAM); + var ram = new RAMPDP10(parmsRAM); + Component.bindComponentControls(ram, eRAM, PDP10.APPCLASS); + } + } +} + +/* + * Initialize all the RAMPDP10 modules on the page. + */ +Web.onInit(RAMPDP10.init); + +if (NODE) module.exports = RAMPDP10; diff --git a/modules/pdp10/lib/rom.js b/modules/pdp10/lib/rom.js new file mode 100644 index 000000000..2bf7ae018 --- /dev/null +++ b/modules/pdp10/lib/rom.js @@ -0,0 +1,339 @@ +/** + * @fileoverview Implements the PDP-10 ROM component. + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +if (NODE) { + var Str = require("../../shared/lib/strlib"); + var Web = require("../../shared/lib/weblib"); + var DumpAPI = require("../../shared/lib/dumpapi"); + var Component = require("../../shared/lib/component"); + var PDP10 = require("./defines"); + var BusPDP10 = require("./bus"); + var MemoryPDP10 = require("./memory"); + var MessagesPDP10 = require("./messages"); +} + +class ROMPDP10 extends Component { + /** + * ROMPDP10(parmsROM) + * + * The ROMPDP10 component expects the following (parmsROM) properties: + * + * addr: physical address of ROM + * size: amount of ROM, in bytes + * alias: physical alias address (null if none) + * file: name of ROM data file + * + * NOTE: The ROM data will not be copied into place until the Bus is ready (see initBus()) AND + * the ROM data file has finished loading (see finishLoad()). + * + * Also, while the size parameter may seem redundant, I consider it useful to confirm that the ROM + * you received is the ROM you expected. + * + * @param {Object} parmsROM + */ + constructor(parmsROM) + { + super("ROM", parmsROM, MessagesPDP10.ROM); + + this.abInit = null; + this.aSymbols = null; + + this.addrROM = +parmsROM['addr']; + this.sizeROM = +parmsROM['size']; + this.fRetainROM = false; + + /* + * The new 'alias' property can now be EITHER a single physical address (like 'addr') OR an array of + * physical addresses; eg: + * + * [0xf0000,0xffff0000,0xffff8000] + * + * We could have overloaded 'addr' to accomplish the same thing, but I think it's better to have any + * aliased locations listed under a separate property. + * + * Most ROMs are not aliased, in which case the 'alias' property should have the default value of null. + */ + this.addrAlias = parmsROM['alias']; + if (typeof this.addrAlias == "string") { + this.addrAlias = eval(this.addrAlias); + } + + this.sFilePath = parmsROM['file']; + this.sFileName = Str.getBaseName(this.sFilePath); + + if (this.sFilePath) { + var sFileURL = this.sFilePath; + if (DEBUG) this.log('load("' + sFileURL + '")'); + /* + * If the selected ROM file has a ".json" extension, then we assume it's pre-converted + * JSON-encoded ROM data, so we load it as-is; ditto for ROM files with a ".hex" extension. + * Otherwise, we ask our server-side ROM converter to return the file in a JSON-compatible format. + */ + var sFileExt = Str.getExtension(this.sFileName); + if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) { + sFileURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true'; + } + var rom = this; + Web.getResource(sFileURL, null, true, function doneLoad(sURL, sResponse, nErrorCode) { + rom.finishLoad(sURL, sResponse, nErrorCode); + }); + } + } + + /** + * initBus(cmp, bus, cpu, dbg) + * + * @this {ROMPDP10} + * @param {ComputerPDP10} cmp + * @param {BusPDP10} bus + * @param {CPUStatePDP10} cpu + * @param {DebuggerPDP10} dbg + */ + initBus(cmp, bus, cpu, dbg) + { + this.bus = bus; + this.cpu = cpu; + this.dbg = dbg; + this.initROM(); + } + + /** + * powerUp(data, fRepower) + * + * @this {ROMPDP10} + * @param {Object|null} data + * @param {boolean} [fRepower] + * @return {boolean} true if successful, false if failure + */ + powerUp(data, fRepower) + { + if (this.aSymbols) { + if (this.dbg) { + this.dbg.addSymbols(this.id, this.addrROM, this.sizeROM, this.aSymbols); + } + /* + * Our only role in the handling of symbols is to hand them off to the Debugger at our + * first opportunity. Now that we've done that, our copy of the symbols, if any, are toast. + */ + delete this.aSymbols; + } + return true; + } + + /** + * powerDown(fSave, fShutdown) + * + * Since we have nothing to do on powerDown(), and no state to return, we could simply omit + * this function. But it doesn't hurt anything, and maybe we'll use our state to save something + * useful down the road, like user-defined symbols (ie, symbols that the Debugger may have + * created, above and beyond those symbols we automatically loaded, if any, along with the ROM). + * + * @this {ROMPDP10} + * @param {boolean} [fSave] + * @param {boolean} [fShutdown] + * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure + */ + powerDown(fSave, fShutdown) + { + return true; + } + + /** + * finishLoad(sURL, sData, nErrorCode) + * + * @this {ROMPDP10} + * @param {string} sURL + * @param {string} sData + * @param {number} nErrorCode (response from server if anything other than 200) + */ + finishLoad(sURL, sData, nErrorCode) + { + if (nErrorCode) { + this.notice("Unable to load ROM resource (error " + nErrorCode + ": " + sURL + ")"); + this.sFilePath = null; + } + else { + Component.addMachineResource(this.idMachine, sURL, sData); + var resource = Web.parseMemoryResource(sURL, sData); + if (resource) { + this.abInit = resource.aBytes; + this.aSymbols = resource.aSymbols; + } else { + this.sFilePath = null; + } + } + this.initROM(); + } + + /** + * initROM() + * + * This function is called by both initBus() and finishLoad(), but it cannot copy the initial data into place + * until after initBus() has received the Bus component AND finishLoad() has received the data. When both those + * criteria are satisfied, the component becomes "ready". + * + * @this {ROMPDP10} + */ + initROM() + { + if (!this.isReady()) { + if (this.sFilePath) { + /* + * Too early... + */ + if (!this.abInit || !this.bus) return; + + /* + * If no explicit size was specified, then use whatever the actual size is. + */ + if (!this.sizeROM) { + this.sizeROM = this.abInit.length; + } + if (this.abInit.length != this.sizeROM) { + /* + * Note that setError() sets the component's fError flag, which in turn prevents setReady() from + * marking the component ready. TODO: Revisit this decision. On the one hand, it sounds like a + * good idea to stop the machine in its tracks whenever a setError() occurs, but there may also be + * times when we'd like to forge ahead anyway. + */ + this.setError("ROM size (" + Str.toHexLong(this.abInit.length) + ") does not match specified size (" + Str.toHexLong(this.sizeROM) + ")"); + } + else if (this.addROM(this.addrROM)) { + + var aliases = []; + if (typeof this.addrAlias == "number") { + aliases.push(this.addrAlias); + } else if (this.addrAlias != null && this.addrAlias.length) { + aliases = this.addrAlias; + } + for (var i = 0; i < aliases.length; i++) { + this.cloneROM(aliases[i]); + } + /* + * We used to hang onto the initial ROM data so that we could restore any bytes the CPU overwrote, + * using memory write-notification handlers, but with the introduction of read-only memory blocks, that's + * no longer necessary. + * + * TODO: Consider an option to retain the ROM data, and give the user some way of restoring ROMs. + * That may be useful for "resumable" machines that save/restore all dirty block of memory, regardless + * whether they're ROM or RAM. However, the only way to modify a machine's ROM is with the Debugger, + * and Debugger users should know better. + */ + if (!this.fRetainROM) { + delete this.abInit; + } + } + } + this.setReady(); + } + } + + /** + * addROM(addr) + * + * @this {ROMPDP10} + * @param {number} addr + * @return {boolean} + */ + addROM(addr) + { + if (this.bus.addMemory(addr, this.sizeROM, MemoryPDP10.TYPE.ROM)) { + if (DEBUG) this.log("addROM(): copying ROM to " + Str.toHexLong(addr) + " (" + Str.toHexLong(this.abInit.length) + " bytes)"); + var i; + for (i = 0; i < this.abInit.length; i++) { + this.bus.setWordDirect(addr + i, this.abInit[i]); + } + return true; + } + + /* + * We don't need to report an error here, because addMemory() already takes care of that. + */ + return false; + } + + /** + * cloneROM(addr) + * + * For ROMs with one or more alias addresses, we used to call addROM() for each address. However, + * that obviously wasted memory, since each alias was an independent copy, and if you used the + * Debugger to edit the ROM in one location, the changes would not appear in the other location(s). + * + * Now that the Bus component provides low-level getMemoryBlocks() and setMemoryBlocks() methods + * to manually get and set the blocks of any memory range, it is now possible to create true aliases. + * + * @this {ROMPDP10} + * @param {number} addr + */ + cloneROM(addr) + { + var aBlocks = this.bus.getMemoryBlocks(this.addrROM, this.sizeROM); + this.bus.setMemoryBlocks(addr, this.sizeROM, aBlocks); + } + + /** + * ROMPDP10.init() + * + * This function operates on every HTML element of class "rom", extracting the + * JSON-encoded parameters for the ROMPDP10 constructor from the element's "data-value" + * attribute, invoking the constructor to create a ROMPDP10 component, and then binding + * any associated HTML controls to the new component. + */ + static init() + { + var aeROM = Component.getElementsByClass(document, PDP10.APPCLASS, "rom"); + for (var iROM = 0; iROM < aeROM.length; iROM++) { + var eROM = aeROM[iROM]; + var parmsROM = Component.getComponentParms(eROM); + var rom = new ROMPDP10(parmsROM); + Component.bindComponentControls(rom, eROM, PDP10.APPCLASS); + } + } +} + +/* + * NOTE: There's currently no need for this component to have a reset() function, since + * once the ROM data is loaded, it can't be changed, so there's nothing to reinitialize. + * + * OK, well, I take that back, because the Debugger, if installed, has the ability to modify + * ROM contents, so in that case, having a reset() function that restores the original ROM data + * might be useful; then again, it might not, depending on what you're trying to debug. + * + * If we do add reset(), then we'll want to change initROM() to hang onto the original + * ROM data; currently, we release it after copying it into the read-only memory allocated + * via bus.addMemory(). + */ + +/* + * Initialize all the ROMPDP10 modules on the page. + */ +Web.onInit(ROMPDP10.init); + +if (NODE) module.exports = ROMPDP10; diff --git a/modules/pdp10/lib/serial.js b/modules/pdp10/lib/serial.js new file mode 100644 index 000000000..a0e31bd0b --- /dev/null +++ b/modules/pdp10/lib/serial.js @@ -0,0 +1,670 @@ +/** + * @fileoverview Implements the PDP-10 SerialPort component + * @author Jeff Parsons + * @copyright © Jeff Parsons 2012-2017 + * + * This file is part of PCjs, a computer emulation software project at . + * + * PCjs is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with PCjs. If not, + * see . + * + * You are required to include the above copyright notice in every modified copy of this work + * and to display that copyright notice when the software starts running; see COPYRIGHT in + * . + * + * Some PCjs files also attempt to load external resource files, such as character-image files, + * ROM files, and disk image files. Those external resource files are not considered part of PCjs + * for purposes of the GNU General Public License, and the author does not claim any copyright + * as to their contents. + */ + +"use strict"; + +if (NODE) { + var Str = require("../../shared/lib/strlib"); + var Web = require("../../shared/lib/weblib"); + var Component = require("../../shared/lib/component"); + var Keys = require("../../shared/lib/keys"); + var State = require("../../shared/lib/state"); + var PDP10 = require("./defines"); + var MessagesPDP10 = require("./messages"); +} + +/** + * Since the Closure Compiler treats ES6 classes as @struct rather than @dict by default, + * it deters us from defining named properties on our components; eg: + * + * this['exports'] = {...} + * + * results in an error: + * + * Cannot do '[]' access on a struct + * + * So, in order to define 'exports', we must override the @struct assumption by annotating + * the class as @unrestricted (or @dict). Note that this must be done both here and in the + * Component class, because otherwise the Compiler won't allow us to *reference* the named + * property either. + * + * TODO: Consider marking ALL our classes unrestricted, because otherwise it forces us to + * define every single property the class uses in its constructor, which results in a fair + * bit of redundant initialization, since many properties aren't (and don't need to be) fully + * initialized until the appropriate init(), reset(), restore(), etc. function is called. + * + * The upside, however, may be that since the structure of the class is completely defined by + * the constructor, JavaScript engines may be able to optimize and run more efficiently. + * + * @unrestricted + */ +class SerialPortPDP10 extends Component { + /** + * SerialPortPDP10(parmsSerial) + * + * The SerialPort component has the following component-specific (parmsSerial) properties: + * + * adapter: adapter number; 0 if not defined (the PCx86 SerialPort component uses this + * value to set the device's internal COM number, which in turn determines other properties, + * such as I/O ports and IRQ; for the PDP-10, this currently has no defined use) + * + * binding: name of a control (based on its "binding" attribute) to bind to this port's I/O + * + * tabSize: set to a non-zero number to convert tabs to spaces (applies only to output to + * the above binding); default is 0 (no conversion) + * + * upperCase: if true, all received input is upper-cased; it is normally the responsibility + * of the sending device to ensure this, but sometimes it's more convenient to enforce + * on the receiving end. + * + * @param {Object} parmsSerial + */ + constructor(parmsSerial) + { + super("SerialPort", parmsSerial, MessagesPDP10.SERIAL); + + this.iAdapter = +parmsSerial['adapter']; + this.fUpperCase = parmsSerial['upperCase']; + if (typeof this.fUpperCase == "string") this.fUpperCase = (this.fUpperCase == "true"); + /** + * consoleOutput becomes a string that records serial port output if the 'binding' property is set to the + * reserved name "console". Nothing is written to the console, however, until a linefeed (0x0A) is output + * or the string length reaches a threshold (currently, 1024 characters). + * + * @type {string|null} + */ + this.consoleOutput = null; + + /** + * controlIOBuffer is a DOM element bound to the port (currently used for output only; see transmitByte()). + * + * Example: CTTY COM2 + * + * The CTTY DOS command redirects all CON I/O to the specified serial port (eg, COM2), which it assumes is + * connected to a serial terminal, and therefore anything it *transmits* via COM2 will be displayed by the + * terminal. It further assumes that anything typed on such a terminal is NOT displayed, so as DOS *receives* + * serial input, DOS *transmits* the appropriate characters back to the terminal via COM2. + * + * As a result, controlIOBuffer only needs to be updated by the transmitByte() function. + * + * @type {Object} + */ + this.controlIOBuffer = null; + + /* + * If controlIOBuffer is being used AND 'tabSize' is set, then we make an attempt to monitor the characters + * being echoed via transmitByte(), maintain a logical column position, and convert any tabs into the appropriate + * number of spaces. + * + * charBOL, if nonzero, is a character to automatically output at the beginning of every line. This probably + * isn't generally useful; I use it internally to preformat serial output. + */ + this.tabSize = +parmsSerial['tabSize']; + this.charBOL = +parmsSerial['charBOL']; + this.iLogicalCol = 0; + this.fNullModem = true; + + this.abReceive = []; + + var sBinding = parmsSerial['binding']; + if (sBinding == "console") { + this.consoleOutput = ""; + } else { + /* + * NOTE: If sBinding is not the name of a valid Control Panel DOM element, this call does nothing. + */ + Component.bindExternalControl(this, sBinding, SerialPortPDP10.sIOBuffer); + } + + /* + * No connection until initConnection() is called. + */ + this.sDataReceived = ""; + this.connection = this.sendData = this.updateStatus = null; + + /* + * Export all functions required by initConnection(). + */ + this['exports'] = { + 'connect': this.initConnection, + 'receiveData': this.receiveData, + 'receiveStatus': this.receiveStatus, + 'setConnection': this.setConnection + }; + } + + /** + * setBinding(sType, sBinding, control, sValue) + * + * @this {SerialPortPDP10} + * @param {string|null} sType is the type of the HTML control (eg, "button", "textarea", "register", "flag", "rled", etc) + * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "buffer") + * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) + * @param {string} [sValue] optional data value + * @return {boolean} true if binding was successful, false if unrecognized binding request + */ + setBinding(sType, sBinding, control, sValue) + { + var serial = this; + + switch (sBinding) { + case SerialPortPDP10.sIOBuffer: + this.bindings[sBinding] = this.controlIOBuffer = control; + + /* + * An onkeydown handler is required for certain keys that browsers tend to consume themselves; + * for example, BACKSPACE is often defined as going back to the previous web page, and certain + * CTRL keys are often used for browser shortcuts (usually on Windows-based browsers). + * + * NOTE: We don't bother with a keyUp handler, because for the most part, we're only intercepting + * keys that require special treatment; in general, we're content with keyPress events. + */ + control.onkeydown = function onKeyDown(event) { + event = event || window.event; + var bASCII = 0; + var keyCode = event.keyCode; + /* + * Perform the same remapping of BACKSPACE and DELETE that our VT100 emulation performs, + * for PCjs-wide consistency; see the KEYMAP table in /modules/pc8080/lib/keyboard.js for + * the rationale. Ditto for ALT-DELETE; see onKeyDown() in /modules/pc8080/lib/keyboard.js + * for details. + * + * NOTE: keyDown (and keyUp) events supply us with KEYCODE values, which are NOT the same as + * ASCII values, which is why we are comparing with KEYCODE values but assigning ASCII values, + * because receiveData() requires ASCII values. + */ + if (keyCode == Keys.KEYCODE.BS) { + bASCII = event.altKey? Keys.ASCII.CTRL_H : Keys.ASCII.DEL; + } + else if (keyCode == Keys.KEYCODE.DEL) { + bASCII = Keys.ASCII.CTRL_H; + } + else if (event.ctrlKey && keyCode >= Keys.ASCII.A && keyCode <= Keys.ASCII.Z) { + bASCII = keyCode - (Keys.ASCII.A - Keys.ASCII.CTRL_A); + } + if (bASCII) { + if (event.preventDefault) event.preventDefault(); + serial.receiveData(bASCII); + } + return true; + }; + + control.onkeypress = function onKeyPress(event) { + /* + * NOTE: Unlike keyDown events, keyPress events generally supply us with ASCII values, + * despite the fact that, as above, they come to us via the keyCode property. Yes, it's + * brilliant (or rather, the opposite of brilliant), but that's life. + */ + event = event || window.event; + /* + * Not sure why COMMAND-key combinations are coming through here (on Safari at least), + * but in any case, let's make sure we don't act on them. + */ + if (!event.metaKey) { + var bASCII = event.which || event.keyCode; + /* + * Perform the same remapping of ALT-ENTER (to LINE-FEED) that our VT100 emulation performs, + * for PCjs-wide consistency; see onKeyDown() in /modules/pc8080/lib/keyboard.js for details. + */ + if (event.altKey) { + if (bASCII == Keys.ASCII.CTRL_M) { + bASCII = Keys.ASCII.CTRL_J; + } + } + serial.receiveData(bASCII); + /* + * Since we're going to remove the "readonly" attribute from the