pcjs/versions/pdpjs/1.35.2/pdp10-uncompiled.js

28203 lines
1,002 KiB
JavaScript

"use strict";
/**
* @copyright http://pcjs.org/modules/shared/lib/defines.js (C) Jeff Parsons 2012-2017
*/
/**
* @define {string}
*/
var APPVERSION = "1.x.x"; // this @define is overridden by the Closure Compiler with the version in package.json
var XMLVERSION = null; // this is set in non-COMPILED builds by embedMachine() if a version number was found in the machine XML
var COPYRIGHT = "Copyright © 2012-2017 Jeff Parsons <Jeff@pcjs.org>";
var LICENSE = "License: GPL version 3 or later <http://gnu.org/licenses/gpl.html>";
var CSSCLASS = "pcjs";
/**
* @define {string}
*/
var SITEHOST = "localhost:8088";// this @define is overridden by the Closure Compiler with "www.pcjs.org"
/**
* @define {boolean}
*/
var COMPILED = false; // this @define is overridden by the Closure Compiler (to true)
/**
* @define {boolean}
*/
var DEBUG = true; // this @define is overridden by the Closure Compiler (to false) to remove DEBUG-only code
/**
* @define {boolean}
*/
var MAXDEBUG = false; // this @define is overridden by the Closure Compiler (to false) to remove MAXDEBUG-only code
/**
* @define {boolean}
*/
var PRIVATE = false; // this @define is overridden by the Closure Compiler (to false) to enable PRIVATE code
/*
* RS-232 DB-25 Pin Definitions, mapped to bits 1-25 in a 32-bit status value.
*
* SerialPorts in PCjs machines are considered DTE (Data Terminal Equipment), which means they should be "virtually"
* connected to each other via a null-modem cable, which assumes the following cross-wiring:
*
* G 1 <-> 1 G (Ground)
* TD 2 <-> 3 RD (Received Data)
* RD 3 <-> 2 TD (Transmitted Data)
* RTS 4 <-> 5 CTS (Clear To Send)
* CTS 5 <-> 4 RTS (Request To Send)
* DSR 6+8 <-> 20 DTR (Data Terminal Ready)
* SG 7 <-> 7 SG (Signal Ground)
* DTR 20 <-> 6+8 DSR (Data Set Ready + Carrier Detect)
* RI 22 <-> 22 RI (Ring Indicator)
*
* TODO: Move these definitions to a more appropriate shared file at some point.
*/
var RS232 = {
RTS: {
PIN: 4,
MASK: 0x00000010
},
CTS: {
PIN: 5,
MASK: 0x00000020
},
DSR: {
PIN: 6,
MASK: 0x00000040
},
CD: {
PIN: 8,
MASK: 0x00000100
},
DTR: {
PIN: 20,
MASK: 0x00100000
},
RI: {
PIN: 22,
MASK: 0x00400000
}
};
/*
* NODE should be true if we're running under NodeJS (eg, command-line), false if not (eg, web browser)
*/
var NODE = false;
/**
* @copyright http://pcjs.org/modules/shared/lib/dumpapi.js (C) Jeff Parsons 2012-2017
*/
/*
* Our "DiskDump API", such as it was, used to look like:
*
* http://jsmachines.net/bin/convdisk.php?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img
*
* To make it (a bit) more "REST-like", the above request now looks like:
*
* http://www.pcjs.org/api/v1/dump?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img
*
* Similarly, our "FileDump API" used to look like:
*
* http://jsmachines.net/bin/convrom.php?rom=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json
*
* and that request now looks like:
*
* http://www.pcjs.org/api/v1/dump?file=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json
*
* I don't think it makes sense to avoid "query" parameters, because blending the path of a disk image with the
* the rest of the URL would be (a) confusing, and (b) more work to parse.
*/
var DumpAPI = {
ENDPOINT: "/api/v1/dump",
QUERY: {
DIR: "dir", // value is path of a directory (DiskDump only)
DISK: "disk", // value is path of a disk image (DiskDump only)
FILE: "file", // value is path of a ROM image file (FileDump only)
IMG: "img", // alias for DISK
PATH: "path", // value is path of a one or more files (DiskDump only)
FORMAT: "format", // value is one of FORMAT values below
COMMENTS: "comments", // value is either "true" or "false"
DECIMAL: "decimal", // value is either "true" to force all numbers to decimal, "false" or undefined otherwise
MBHD: "mbhd", // value is hard drive size in Mb (formerly "mbsize") (DiskDump only) (DEPRECATED)
SIZE: "size" // value is target disk size in Kb (supersedes "mbhd") (DiskDump only)
},
FORMAT: {
JSON: "json", // default
JSON_GZ: "gz", // gzip is currently used ONLY for compressed JSON
DATA: "data", // same as "json", but built without JSON.stringify() (DiskDump only)
HEX: "hex", // deprecated
OCTAL: "octal", // displays data as octal words
BYTES: "bytes", // displays data as hex bytes; normally used only when comments are enabled
WORDS: "words", // displays data as hex words; normally used only when comments are enabled
LONGS: "longs", // displays data as dwords
IMG: "img", // returns the raw disk data (ie, using a Buffer object) (DiskDump only)
ROM: "rom" // returns the raw file data (ie, using a Buffer object) (FileDump only)
}
};
/*
* Because we use an overloaded API endpoint (ie, one that's shared with the FileDump module), we must
* also provide a list of commands which, when combined with the endpoint, define a unique request.
*/
DumpAPI.asDiskCommands = [DumpAPI.QUERY.DIR, DumpAPI.QUERY.DISK, DumpAPI.QUERY.PATH];
DumpAPI.asFileCommands = [DumpAPI.QUERY.FILE];
/**
* @copyright http://pcjs.org/modules/shared/lib/reportapi.js (C) Jeff Parsons 2012-2017
*/
var ReportAPI = {
ENDPOINT: "/api/v1/report",
QUERY: {
APP: "app",
VER: "ver",
URL: "url",
USER: "user",
TYPE: "type",
DATA: "data"
},
TYPE: {
BUG: "bug"
},
RES: {
OK: "Thank you"
}
};
/**
* @copyright http://pcjs.org/modules/shared/lib/userapi.js (C) Jeff Parsons 2012-2017
*/
/*
* Examples of User API requests:
*
* web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUser;
*/
var UserAPI = {
ENDPOINT: "/api/v1/user",
QUERY: {
REQ: "req", // specifies a request
USER: "user", // specifies a user ID
STATE: "state", // specifies a state ID
DATA: "data" // specifies state data
},
REQ: {
CREATE: "create", // creates a user ID
VERIFY: "verify", // requests verification of a user ID
STORE: "store", // stores a machine state on the server
LOAD: "load" // loads a machine state from the server
},
RES: {
CODE: "code",
DATA: "data"
},
CODE: {
OK: "ok",
FAIL: "error"
},
FAIL: {
DUPLICATE: "user already exists",
VERIFY: "unable to verify user",
BADSTATE: "invalid state parameter",
NOSTATE: "no machine state",
BADLOAD: "unable to load machine state",
BADSTORE: "unable to save machine state"
}
};
/**
* @copyright http://pcjs.org/modules/shared/lib/keys.js (C) Jeff Parsons 2012-2017
*/
var Keys = {
/*
* Keys and/or key combinations that generate common ASCII codes.
*
* NOTE: If you're looking for a general-purpose ASCII code table, see Str.ASCII in strlib.js;
* if something's missing, that's probably the more appropriate table to add it to.
*
* TODO: The Closure Compiler doesn't inline all references to these values, at least those with
* quoted property names, which is why I've 'unquoted' as many of them as possible. One solution
* would be to add mnemonics for all of them, not just the non-printable ones (eg, SPACE instead
* of ' ', AMP instead of '&', etc.)
*/
ASCII: {
BREAK: 0, CTRL_A: 1, CTRL_B: 2, CTRL_C: 3, CTRL_D: 4, CTRL_E: 5, CTRL_F: 6, CTRL_G: 7,
CTRL_H: 8, CTRL_I: 9, CTRL_J: 10, CTRL_K: 11, CTRL_L: 12, CTRL_M: 13, CTRL_N: 14, CTRL_O: 15,
CTRL_P: 16, CTRL_Q: 17, CTRL_R: 18, CTRL_S: 19, CTRL_T: 20, CTRL_U: 21, CTRL_V: 22, CTRL_W: 23,
CTRL_X: 24, CTRL_Y: 25, CTRL_Z: 26,
' ': 32, '!': 33, '"': 34, '#': 35, '$': 36, '%': 37, '&': 38, "'": 39,
'(': 40, ')': 41, '*': 42, '+': 43, ',': 44, '-': 45, '.': 46, '/': 47,
'0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55,
'8': 56, '9': 57, ':': 58, ';': 59, '<': 60, '=': 61, '>': 62, '?': 63,
'@': 64, A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71,
H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79,
P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87,
X: 88, Y: 89, Z: 90, '[': 91, '\\':92, ']': 93, '^': 94, '_': 95,
'`': 96, a: 97, b: 98, c: 99, d: 100, e: 101, f: 102, g: 103,
h: 104, i: 105, j: 106, k: 107, l: 108, m: 109, n: 110, o: 111,
p: 112, q: 113, r: 114, s: 115, t: 116, u: 117, v: 118, w: 119,
x: 120, y: 121, z: 122, '{':123, '|':124, '}':125, '~':126, DEL: 127
},
/*
* Browser keyCodes we must pay particular attention to. For the most part, these are non-alphanumeric
* or function keys, some which may require special treatment (eg, preventDefault() if returning false on
* the initial keyDown event is insufficient).
*
* keyCodes for most common ASCII keys can simply use the appropriate ASCII code above.
*
* Most of these represent non-ASCII keys (eg, the LEFT arrow key), yet for some reason, browsers defined
* them using ASCII codes (eg, the LEFT arrow key uses the ASCII code for '%' or 37).
*/
KEYCODE: {
/* 0x08 */ BS: 8, // BACKSPACE (ASCII.CTRL_H)
/* 0x09 */ TAB: 9, // TAB (ASCII.CTRL_I)
/* 0x0A */ LF: 10, // LINE FEED (ASCII.CTRL_J) (TODO: Determine if any key actually generates this)
/* 0x0D */ CR: 13, // CARRIAGE RETURN (ASCII.CTRL_M)
/* 0x10 */ SHIFT: 16,
/* 0x11 */ CTRL: 17,
/* 0x12 */ ALT: 18,
/* 0x13 */ PAUSE: 19, // PAUSE/BREAK
/* 0x14 */ CAPS_LOCK: 20,
/* 0x1B */ ESC: 27,
/* 0x20 */ SPACE: 32,
/* 0x21 */ PGUP: 33,
/* 0x22 */ PGDN: 34,
/* 0x23 */ END: 35,
/* 0x24 */ HOME: 36,
/* 0x25 */ LEFT: 37,
/* 0x26 */ UP: 38,
/* 0x27 */ RIGHT: 39,
/* 0x27 */ FF_QUOTE: 39,
/* 0x28 */ DOWN: 40,
/* 0x2C */ FF_COMMA: 44,
/* 0x2C */ PRTSC: 44,
/* 0x2D */ INS: 45,
/* 0x2E */ DEL: 46,
/* 0x2E */ FF_PERIOD: 46,
/* 0x2F */ FF_SLASH: 47,
/* 0x30 */ ZERO: 48,
/* 0x31 */ ONE: 49,
/* 0x32 */ TWO: 50,
/* 0x33 */ THREE: 51,
/* 0x34 */ FOUR: 52,
/* 0x35 */ FIVE: 53,
/* 0x36 */ SIX: 54,
/* 0x37 */ SEVEN: 55,
/* 0x38 */ EIGHT: 56,
/* 0x39 */ NINE: 57,
/* 0x3B */ FF_SEMI: 59,
/* 0x3D */ FF_EQUALS: 61,
/* 0x5B */ CMD: 91, // aka WIN
/* 0x5B */ FF_LBRACK: 91,
/* 0x5C */ FF_BSLASH: 92,
/* 0x5D */ RCMD: 93, // aka MENU
/* 0x5D */ FF_RBRACK: 93,
/* 0x60 */ NUM_0: 96,
/* 0x60 */ NUM_INS: 96,
/* 0x60 */ FF_BQUOTE: 96,
/* 0x61 */ NUM_1: 97,
/* 0x61 */ NUM_END: 97,
/* 0x62 */ NUM_2: 98,
/* 0x62 */ NUM_DOWN: 98,
/* 0x63 */ NUM_3: 99,
/* 0x63 */ NUM_PGDN: 99,
/* 0x64 */ NUM_4: 100,
/* 0x64 */ NUM_LEFT: 100,
/* 0x65 */ NUM_5: 101,
/* 0x65 */ NUM_CENTER: 101,
/* 0x66 */ NUM_6: 102,
/* 0x66 */ NUM_RIGHT: 102,
/* 0x67 */ NUM_7: 103,
/* 0x67 */ NUM_HOME: 103,
/* 0x68 */ NUM_8: 104,
/* 0x68 */ NUM_UP: 104,
/* 0x69 */ NUM_9: 105,
/* 0x69 */ NUM_PGUP: 105,
/* 0x6A */ NUM_MUL: 106,
/* 0x6B */ NUM_ADD: 107,
/* 0x6D */ NUM_SUB: 109,
/* 0x6E */ NUM_DEL: 110, // aka PERIOD
/* 0x6F */ NUM_DIV: 111,
/* 0x70 */ F1: 112,
/* 0x71 */ F2: 113,
/* 0x72 */ F3: 114,
/* 0x73 */ F4: 115,
/* 0x74 */ F5: 116,
/* 0x75 */ F6: 117,
/* 0x76 */ F7: 118,
/* 0x77 */ F8: 119,
/* 0x78 */ F9: 120,
/* 0x79 */ F10: 121,
/* 0x7A */ F11: 122,
/* 0x7B */ F12: 123,
/* 0x90 */ NUM_LOCK: 144,
/* 0x91 */ SCROLL_LOCK: 145,
/* 0xAD */ FF_DASH: 173,
/* 0xBA */ SEMI: 186, // Firefox: 59 (FF_SEMI)
/* 0xBB */ EQUALS: 187, // Firefox: 61 (FF_EQUALS)
/* 0xBC */ COMMA: 188,
/* 0xBD */ DASH: 189, // Firefox: 173 (FF_DASH)
/* 0xBE */ PERIOD: 190,
/* 0xBF */ SLASH: 191,
/* 0xC0 */ BQUOTE: 192,
/* 0xDB */ LBRACK: 219,
/* 0xDC */ BSLASH: 220,
/* 0xDD */ RBRACK: 221,
/* 0xDE */ QUOTE: 222,
/* 0xE0 */ FF_CMD: 224, // Firefox only (used for both CMD and RCMD)
//
// The following biases use what I'll call Decimal Coded Binary or DCB (the opposite of BCD),
// where the thousands digit is used to store the sum of "binary" digits 1 and/or 2 and/or 4.
//
// Technically, that makes it DCO (Decimal Coded Octal), but then again, BCD should have really
// been called HCD (Hexadecimal Coded Decimal), so if "they" can take liberties, so can I.
//
// ONDOWN is a bias we add to browser keyCodes that we want to handle on "down" rather than on "press".
//
ONDOWN: 1000,
//
// ONRIGHT is a bias we add to browser keyCodes that need to check for a "right" location (default is "left")
//
ONRIGHT: 2000,
//
// FAKE is a bias we add to signal these are fake keyCodes corresponding to internal keystroke combinations.
// The actual values are for internal use only and merely need to be unique and used consistently.
//
FAKE: 4000
},
/*
* The set of values that a browser may store in the 'location' property of a keyboard event object
* which we also support.
*/
LOCATION: {
LEFT: 1,
RIGHT: 2,
NUMPAD: 3
}
};
/*
* Check the event object's 'location' property for a non-zero value for the following ONRIGHT keys.
*/
Keys.KEYCODE.NUM_CR = Keys.KEYCODE.CR + Keys.KEYCODE.ONRIGHT;
/*
* Maps Firefox keyCodes to their more common keyCode counterparts; a number of entries in this table
* are no longer valid (if indeed they ever were), so they've been commented out. It's likely that I
* simply extended this table to resolve additional differences in other browsers (ie, Opera), but without
* browser-specific checks, it's not safe to perform all the mappings shown below.
*/
Keys.FF_KEYCODES = {};
Keys.FF_KEYCODES[Keys.KEYCODE.FF_SEMI] = Keys.KEYCODE.SEMI; // 59 -> 186
Keys.FF_KEYCODES[Keys.KEYCODE.FF_EQUALS] = Keys.KEYCODE.EQUALS; // 61 -> 187
Keys.FF_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.KEYCODE.DASH; // 173 -> 189
Keys.FF_KEYCODES[Keys.KEYCODE.FF_CMD] = Keys.KEYCODE.CMD; // 224 -> 91
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_COMMA] = Keys.KEYCODE.COMMA; // 44 -> 188
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_PERIOD] = Keys.KEYCODE.PERIOD; // 46 -> 190
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_SLASH] = Keys.KEYCODE.SLASH; // 47 -> 191
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_BQUOTE] = Keys.KEYCODE.BQUOTE; // 96 -> 192
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_LBRACK = Keys.KEYCODE.LBRACK; // 91 -> 219
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_BSLASH] = Keys.KEYCODE.BSLASH; // 92 -> 220
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_RBRACK] = Keys.KEYCODE.RBRACK; // 93 -> 221
// Keys.FF_KEYCODES[Keys.KEYCODE.FF_QUOTE] = Keys.KEYCODE.QUOTE; // 39 -> 222
/*
* Maps non-ASCII keyCodes to their ASCII counterparts
*/
Keys.NONASCII_KEYCODES = {};
Keys.NONASCII_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['-']; // 173 -> 45
Keys.NONASCII_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[';']; // 186 -> 59
Keys.NONASCII_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['=']; // 187 -> 61
Keys.NONASCII_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['-']; // 189 -> 45
Keys.NONASCII_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII[',']; // 188 -> 44
Keys.NONASCII_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['.']; // 190 -> 46
Keys.NONASCII_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['/']; // 191 -> 47
Keys.NONASCII_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['`']; // 192 -> 96
Keys.NONASCII_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['[']; // 219 -> 91
Keys.NONASCII_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['\\']; // 220 -> 92
Keys.NONASCII_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII[']']; // 221 -> 93
Keys.NONASCII_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII["'"]; // 222 -> 39
/*
* Maps unshifted keyCodes to their shifted counterparts; to be used when a shift-key is down.
* Alphabetic characters are handled in code, since they must also take CAPS_LOCK into consideration.
*/
Keys.SHIFTED_KEYCODES = {};
Keys.SHIFTED_KEYCODES[Keys.ASCII['1']] = Keys.ASCII['!'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['2']] = Keys.ASCII['@'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['3']] = Keys.ASCII['#'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['4']] = Keys.ASCII['$'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['5']] = Keys.ASCII['%'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['6']] = Keys.ASCII['^'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['7']] = Keys.ASCII['&'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['8']] = Keys.ASCII['*'];
Keys.SHIFTED_KEYCODES[Keys.ASCII['9']] = Keys.ASCII['('];
Keys.SHIFTED_KEYCODES[Keys.ASCII['0']] = Keys.ASCII[')'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.SEMI] = Keys.ASCII[':'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.EQUALS] = Keys.ASCII['+'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.COMMA] = Keys.ASCII['<'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.DASH] = Keys.ASCII['_'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.PERIOD] = Keys.ASCII['>'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.SLASH] = Keys.ASCII['?'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.BQUOTE] = Keys.ASCII['~'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.LBRACK] = Keys.ASCII['{'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.BSLASH] = Keys.ASCII['|'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.RBRACK] = Keys.ASCII['}'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.QUOTE] = Keys.ASCII['"'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_DASH] = Keys.ASCII['_'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_EQUALS] = Keys.ASCII['+'];
Keys.SHIFTED_KEYCODES[Keys.KEYCODE.FF_SEMI] = Keys.ASCII[':'];
/**
* @copyright http://pcjs.org/modules/shared/lib/strlib.js (C) Jeff Parsons 2012-2017
*/
class Str {
/**
* isValidInt(s, base)
*
* The built-in parseInt() function has the annoying feature of returning a partial value (ie,
* up to the point where it encounters an invalid character); eg, parseInt("foo", 16) returns 0xf.
*
* So it's best to use our own Str.parseInt() function, which will in turn use this function to
* validate the entire string.
*
* @param {string} s is the string representation of some number
* @param {number} [base] is the radix to use (default is 10); only 2, 8, 10 and 16 are supported
* @return {boolean} true if valid, false if invalid (or the specified base isn't supported)
*/
static isValidInt(s, base)
{
if (!base || base == 10) return s.match(/^-?[0-9]+$/) !== null;
if (base == 16) return s.match(/^-?[0-9a-f]+$/i) !== null;
if (base == 8) return s.match(/^-?[0-7]+$/) !== null;
if (base == 2) return s.match(/^-?[01]+$/) !== null;
return false;
}
/**
* parseInt(s, base)
*
* This is a wrapper around the built-in parseInt() function. Our wrapper recognizes certain prefixes
* ('$' or "0x" for hex, '#' or "0o" for octal) and suffixes ('.' for decimal, 'h' for hex, 'y' for
* binary), and then calls isValidInt() to ensure we don't convert strings that contain partial values;
* see isValidInt() for details.
*
* The use of multiple prefix/suffix combinations is undefined (although for the record, we process
* prefixes first). We do NOT support the "0b" prefix to indicate binary UNLESS one or more commas are
* also present (because "0b" is also a valid hex sequence), and we do NOT support a single leading zero
* to indicate octal (because such a number could also be decimal or hex). Any number of commas are
* allowed; we remove them all before calling the built-in parseInt().
*
* More recently, we've added support for "^D", "^O", and "^B" prefixes to accommodate the base overrides
* that the PDP-10's MACRO-10 assembly language supports (decimal, octal, and binary, respectively).
* If this support turns out to adversely affect other debuggers, then it will have to be "conditionalized".
* Similarly, we've added support for "K", "M", and "G" MACRO-10-style suffixes that add 3, 6, or 9 zeros
* to the value to be parsed, respectively.
*
* @param {string} s is the string representation of some number
* @param {number} [base] is the radix to use (default is 10); can be overridden by prefixes/suffixes
* @return {number|undefined} corresponding value, or undefined if invalid
*/
static parseInt(s, base)
{
var value;
if (s) {
if (!base) base = 10;
var ch, chPrefix, chSuffix;
var fCommas = (s.indexOf(',') > 0);
if (fCommas) s = s.replace(/,/g, '');
ch = chPrefix = s.charAt(0);
if (chPrefix == '#') {
base = 8;
chPrefix = '';
}
else if (chPrefix == '$') {
base = 16;
chPrefix = '';
}
if (ch != chPrefix) {
s = s.substr(1);
}
else {
ch = chPrefix = s.substr(0, 2);
if (chPrefix == '0b' && fCommas || chPrefix == '^B') {
base = 2;
chPrefix = '';
}
else if (chPrefix == '0o' || chPrefix == '^O') {
base = 8;
chPrefix = '';
}
else if (chPrefix == '^D') {
base = 10;
chPrefix = '';
}
else if (chPrefix == '0x') {
base = 16;
chPrefix = '';
}
if (ch != chPrefix) s = s.substr(2);
}
ch = chSuffix = s.slice(-1);
if (chSuffix == 'Y' || chSuffix == 'y') {
base = 2;
chSuffix = '';
}
else if (chSuffix == '.') {
base = 10;
chSuffix = '';
}
else if (chSuffix == 'H' || chSuffix == 'h') {
base = 16;
chSuffix = '';
}
else if (chSuffix == 'K') {
chSuffix = '000';
}
else if (chSuffix == 'M') {
chSuffix = '000000';
}
else if (chSuffix == 'G') {
chSuffix = '000000000';
}
if (ch != chSuffix) s = s.slice(0, -1) + chSuffix;
/*
* This adds support for the MACRO-10 binary shifting (Bn) suffix, which must be stripped from the
* number before parsing, and then applied to the value after parsing. If n is omitted, 35 is assumed,
* which is a net shift of zero. If n < 35, then a left shift of (35 - n) is required; if n > 35, then
* a right shift of -(35 - n) is required.
*/
var v, shift = 0;
if (base <= 10) {
var match = s.match(/(-?[0-9]+)B([0-9]*)/);
if (match) {
s = match[1];
shift = 35 - ((match[2] || 35) & 0xff);
}
}
if (Str.isValidInt(s, base) && !isNaN(v = parseInt(s, base))) {
/*
* With the need to support larger (eg, 36-bit) integers, truncating to 32 bits is no longer helpful.
*
* value = v|0;
*/
if (shift) {
/*
* Since binary shifting is a logical operation, and since shifting by division only works properly
* with positive numbers, we must convert a negative value to a positive value, by computing the two's
* complement.
*/
if (v < 0) v += Math.pow(2, 36);
if (shift > 0) {
v *= Math.pow(2, shift);
} else {
v = Math.trunc(v / Math.pow(2, -shift));
}
}
value = v;
}
}
return value;
}
/**
* toBase(n, radix, cch, sPrefix, nGrouping)
*
* Displays the given number as an unsigned integer using the specified radix and number of digits.
*
* @param {number|null|undefined} n
* @param {number} radix (ie, the base)
* @param {number} cch (the desired number of digits)
* @param {string} [sPrefix] (default is none)
* @param {number} [nGrouping]
* @return {string}
*/
static toBase(n, radix, cch, sPrefix = "", nGrouping = 0)
{
/*
* An initial "falsey" check for null takes care of both null and undefined;
* we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough.
*
* Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN,
* since JavaScript coerces such operands to zero, but I think there's "value" in seeing those
* values displayed differently.
*/
var s = "";
if (isNaN(n)) {
n = null;
} else if (n != null) {
/*
* Callers that produced an input by dividing by a power of two rather than shifting (in order
* to access more than 32 bits) may produce a fractional result, which ordinarily we would simply
* ignore, but if the integer portion is zero and the sign is negative, we should probably treat
* this value as a sign-extension.
*/
if (n < 0 && n > -1) n = -1;
/*
* Negative values should be two's complemented according to the number of digits; for example,
* 12 octal digits implies an upper limit 8^12.
*/
if (n < 0) {
n += Math.pow(radix, cch);
}
if (n >= Math.pow(radix, cch)) {
cch = Math.ceil(Math.log(n) / Math.log(radix));
}
}
var g = nGrouping || -1;
while (cch-- > 0) {
if (!g) {
s = ',' + s;
g = nGrouping;
}
if (n == null) {
s = '?' + s;
} else {
var d = n % radix;
d += (d >= 0 && d <= 9? 0x30 : 0x41 - 10);
s = String.fromCharCode(d) + s;
n = Math.trunc(n / radix);
}
g--;
}
return sPrefix + s;
}
/**
* toBin(n, cch, nGrouping)
*
* Converts an integer to binary, with the specified number of digits (up to a maximum of 36).
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of binary digits (0 or undefined for default of either 8, 18, or 36)
* @param {number} [nGrouping]
* @return {string} the binary representation of n
*/
static toBin(n, cch, nGrouping)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN2) || 1;
var v = Math.abs(n);
if (v <= 0b11111111) {
cch = 8;
} else if (v <= 0b111111111111111111) {
cch = 18;
} else {
cch = 36;
}
} else if (cch > 36) cch = 36;
return Str.toBase(n, 2, cch, "", nGrouping);
}
/**
* toBinBytes(n, cb, fPrefix)
*
* Converts an integer to binary, with the specified number of bytes (up to the default of 4).
*
* @param {number|null|undefined} n (interpreted as a 32-bit value)
* @param {number} [cb] is the desired number of binary bytes (4 is both the default and the maximum)
* @param {boolean} [fPrefix]
* @return {string} the binary representation of n
*/
static toBinBytes(n, cb, fPrefix)
{
var s = "";
if (!cb || cb > 4) cb = 4;
for (var i = 0; i < cb; i++) {
if (s) s = ',' + s;
s = Str.toBin(n & 0xff, 8) + s;
n >>= 8;
}
return (fPrefix? "0b" : "") + s;
}
/**
* toOct(n, cch, fPrefix)
*
* Converts an integer to octal, with the specified number of digits (default of 6; max of 12)
*
* You might be tempted to use the built-in n.toString(8) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw
* an exception, whereas this function will return '?' characters.
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of octal digits (0 or undefined for default of either 6, 8, or 12)
* @param {boolean} [fPrefix]
* @return {string} the octal representation of n
*/
static toOct(n, cch, fPrefix)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(8)) || 1;
var v = Math.abs(n);
if (v <= 0o777777) {
cch = 6;
} else if (v <= 0o77777777) {
cch = 8;
} else {
cch = 12;
}
} else if (cch > 12) cch = 12;
return Str.toBase(n, 8, cch, fPrefix? "0o" : "");
}
/**
* toDec(n, cch)
*
* Converts an integer to decimal, with the specified number of digits (default of 5; max of 11)
*
* You might be tempted to use the built-in n.toString(10) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values. Moreover, if n is undefined, n.toString() will throw
* an exception, whereas this function will return '?' characters.
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of decimal digits (0 or undefined for default of either 5 or 11)
* @return {string} the decimal representation of n
*/
static toDec(n, cch)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.LN10) || 1;
var v = Math.abs(n);
if (v <= 99999) {
cch = 5;
} else {
cch = 11;
}
} else if (cch > 11) cch = 11;
return Str.toBase(n, 10, cch);
}
/**
* toHex(n, cch, fPrefix)
*
* Converts an integer to hex, with the specified number of digits (default of 4 or 8, max of 9).
*
* You might be tempted to use the built-in n.toString(16) instead, but it doesn't zero-pad and it
* doesn't properly convert negative values; for example, if n is -2147483647, then n.toString(16)
* will return "-7fffffff" instead of "80000001". Moreover, if n is undefined, n.toString() will
* throw an exception, whereas this function will return '?' characters.
*
* NOTE: The following work-around (adapted from code found on StackOverflow) would be another solution,
* taking care of negative values, zero-padding, and upper-casing, but not null/undefined/NaN values:
*
* s = (n < 0? n + 0x100000000 : n).toString(16);
* s = "00000000".substr(0, 8 - s.length) + s;
* s = s.substr(0, cch).toUpperCase();
*
* @param {number|null|undefined} n (supports integers up to 36 bits now)
* @param {number} [cch] is the desired number of hex digits (0 or undefined for default of either 4, 8, or 9)
* @param {boolean} [fPrefix]
* @return {string} the hex representation of n
*/
static toHex(n, cch, fPrefix)
{
if (!cch) {
// cch = Math.ceil(Math.log(Math.abs(n) + 1) / Math.log(16)) || 1;
var v = Math.abs(n);
if (v <= 0xffff) {
cch = 4;
} else if (v <= 0xffffffff) {
cch = 8;
} else {
cch = 9;
}
} else if (cch > 9) cch = 9;
return Str.toBase(n, 16, cch, fPrefix? "0x" : "");
}
/**
* toHexByte(b)
*
* Alias for Str.toHex(b, 2, true)
*
* @param {number|null|undefined} b is a byte value
* @return {string} the hex representation of b
*/
static toHexByte(b)
{
return Str.toHex(b, 2, true);
}
/**
* toHexWord(w)
*
* Alias for Str.toHex(w, 4, true)
*
* @param {number|null|undefined} w is a word (16-bit) value
* @return {string} the hex representation of w
*/
static toHexWord(w)
{
return Str.toHex(w, 4, true);
}
/**
* toHexLong(l)
*
* Alias for Str.toHex(l, 8, true)
*
* @param {number|null|undefined} l is a dword (32-bit) value
* @return {string} the hex representation of w
*/
static toHexLong(l)
{
return Str.toHex(l, 8, true);
}
/**
* getBaseName(sFileName, fStripExt)
*
* This is a poor-man's version of Node's path.basename(), which Node-only components should use instead.
*
* Note that if fStripExt is true, this strips ANY extension, whereas path.basename() strips the extension only
* if it matches the second parameter (eg, path.basename("/foo/bar/baz/asdf/quux.html", ".html") returns "quux").
*
* @param {string} sFileName
* @param {boolean} [fStripExt]
* @return {string}
*/
static getBaseName(sFileName, fStripExt)
{
var sBaseName = sFileName;
var i = sFileName.lastIndexOf('/');
if (i >= 0) sBaseName = sFileName.substr(i + 1);
/*
* This next bit is a kludge to clean up names that are part of a URL that includes unsightly query parameters.
*/
i = sBaseName.indexOf('&');
if (i > 0) sBaseName = sBaseName.substr(0, i);
if (fStripExt) {
i = sBaseName.lastIndexOf(".");
if (i > 0) {
sBaseName = sBaseName.substring(0, i);
}
}
return sBaseName;
}
/**
* getExtension(sFileName)
*
* This is a poor-man's version of Node's path.extname(), which Node-only components should use instead.
*
* Note that we EXCLUDE the period from the returned extension, whereas path.extname() includes it.
*
* @param {string} sFileName
* @return {string} the filename's extension (in lower-case and EXCLUDING the "."), or an empty string
*/
static getExtension(sFileName)
{
var sExtension = "";
var i = sFileName.lastIndexOf(".");
if (i >= 0) {
sExtension = sFileName.substr(i + 1).toLowerCase();
}
return sExtension;
}
/**
* endsWith(s, sSuffix)
*
* @param {string} s
* @param {string} sSuffix
* @return {boolean} true if s ends with sSuffix, false if not
*/
static endsWith(s, sSuffix)
{
return s.indexOf(sSuffix, s.length - sSuffix.length) !== -1;
}
/**
* escapeHTML(sHTML)
*
* @param {string} sHTML
* @return {string} with HTML entities "escaped", similar to PHP's htmlspecialchars()
*/
static escapeHTML(sHTML)
{
return sHTML.replace(/[&<>"']/g, function(m)
{
return Str.aHTMLEscapeMap[m];
});
}
/**
* replace(sSearch, sReplace, s)
*
* The JavaScript replace() function ALWAYS interprets "$" specially in replacement strings, even when
* the search string is NOT a RegExp; specifically:
*
* $$ Inserts a "$"
* $& Inserts the matched substring
* $` Inserts the portion of the string that precedes the matched substring
* $' Inserts the portion of the string that follows the matched substring
* $n Where n is a positive integer less than 100, inserts the nth parenthesized sub-match string,
* provided the first argument was a RegExp object
*
* So, if a replacement string containing dollar signs passes through a series of replace() calls, untold
* problems could result. Hence, this function, which simply uses the replacement string as-is.
*
* Similar to the JavaScript replace() method (when sSearch is a string), this replaces only ONE occurrence
* (ie, the FIRST occurrence); it might be nice to add options to replace the LAST occurrence and/or ALL
* occurrences, but we'll revisit that later.
*
* @param {string} sSearch
* @param {string} sReplace
* @param {string} s
* @return {string}
*/
static replace(sSearch, sReplace, s)
{
var i = s.indexOf(sSearch);
if (i >= 0) {
s = s.substr(0, i) + sReplace + s.substr(i + sSearch.length);
}
return s;
}
/**
* replaceAll(sSearch, sReplace, s)
*
* @param {string} sSearch
* @param {string} sReplace
* @param {string} s
* @return {string}
*/
static replaceAll(sSearch, sReplace, s)
{
var a = {};
a[sSearch] = sReplace;
return Str.replaceArray(a, s);
}
/**
* replaceArray(a, s)
*
* @param {Object} a
* @param {string} s
* @return {string}
*/
static replaceArray(a, s)
{
var sMatch = "";
for (var k in a) {
/*
* As noted in:
*
* http://www.regexguru.com/2008/04/escape-characters-only-when-necessary/
*
* inside character classes, only backslash, caret, hyphen and the closing bracket need to be
* escaped. And in fact, if you ensure that the closing bracket is first, the caret is not first,
* and the hyphen is last, you can avoid escaping those as well.
*/
k = k.replace(/([\\[\]*{}().+?])/g, "\\$1");
sMatch += (sMatch? '|' : '') + k;
}
return s.replace(new RegExp('(' + sMatch + ')', "g"), function(m)
{
return a[m];
});
}
/**
* pad(s, cch, fPadLeft)
*
* NOTE: the maximum amount of padding currently supported is 40 spaces.
*
* @param {string} s is a string
* @param {number} cch is desired length
* @param {boolean} [fPadLeft] (default is padding on the right)
* @return {string} the original string (s) with spaces padding it to the specified length
*/
static pad(s, cch, fPadLeft)
{
var sPadding = " ";
return fPadLeft? (sPadding + s).slice(-cch) : (s + sPadding).slice(0, cch);
}
/**
* stripLeadingZeros(s, fPad)
*
* @param {string} s
* @param {boolean} [fPad]
* @return {string}
*/
static stripLeadingZeros(s, fPad)
{
var cch = s.length;
s = s.replace(/^0+([0-9A-F]+)$/i, "$1");
if (fPad) s = Str.pad(s, cch, true);
return s;
}
/**
* trim(s)
*
* @param {string} s
* @return {string}
*/
static trim(s)
{
if (String.prototype.trim) {
return s.trim();
}
return s.replace(/^\s+|\s+$/g, "");
}
/**
* toASCIICode(b)
*
* @param {number} b
* @return {string}
*/
static toASCIICode(b)
{
var s;
if (b != Str.ASCII.CR && b != Str.ASCII.LF) {
s = Str.aASCIICodes[b];
}
if (s) {
s = '<' + s + '>';
} else {
s = String.fromCharCode(b);
}
return s;
}
}
Str.aHTMLEscapeMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
/*
* Future home of a general-purpose ASCII table. TODO: Flesh it out.
*/
Str.ASCII = {
LF: 0x0A,
CR: 0x0D
};
/*
* Table for converting "unprintable" ASCII codes into mnemonics, to more clearly see what's being printed.
*/
Str.aASCIICodes = {
0x00: "NUL",
0x01: "SOH", // (CTRL_A) Start of Heading
0x02: "STX", // (CTRL_B) Start of Text
0x03: "ETX", // (CTRL_C) End of Text
0x04: "EOT", // (CTRL_D) End of Transmission
0x05: "ENQ", // (CTRL_E) Enquiry
0x06: "ACK", // (CTRL_F) Acknowledge
0x07: "BEL", // (CTRL_G) Bell
0x08: "BS", // (CTRL_H) Backspace
0x09: "TAB", // (CTRL_I) Horizontal Tab
0x0A: "LF", // (CTRL_J) Line Feed (New Line)
0x0B: "VT", // (CTRL_K) Vertical Tab
0x0C: "FF", // (CTRL_L) Form Feed (New Page)
0x0D: "CR", // (CTRL_M) Carriage Return
0x0E: "SO", // (CTRL_N) Shift Out
0x0F: "SI", // (CTRL_O) Shift In
0x10: "DLE", // (CTRL_P) Data Link Escape
0x11: "XON", // (CTRL_Q) Device Control 1 (aka DC1)
0x12: "DC2", // (CTRL_R) Device Control 2
0x13: "XOFF", // (CTRL_S) Device Control 3 (aka DC3)
0x14: "DC4", // (CTRL_T) Device Control 4
0x15: "NAK", // (CTRL_U) Negative Acknowledge
0x16: "SYN", // (CTRL_V) Synchronous Idle
0x17: "ETB", // (CTRL_W) End of Transmission Block
0x18: "CAN", // (CTRL_X) Cancel
0x19: "EM", // (CTRL_Y) End of Medium
0x1A: "SUB", // (CTRL_Z) Substitute
0x1B: "ESC", // Escape
0x1C: "FS", // File Separator
0x1D: "GS", // Group Separator
0x1E: "RS", // Record Separator
0x1F: "US" // Unit Separator
};
Str.TYPES = {
NULL: 0,
BYTE: 1,
WORD: 2,
DWORD: 3,
NUMBER: 4,
STRING: 5,
BOOLEAN: 6,
OBJECT: 7,
ARRAY: 8
};
/**
* @copyright http://pcjs.org/modules/shared/lib/usrlib.js (C) Jeff Parsons 2012-2017
*/
/**
* @typedef {{
* mask: number,
* shift: number
* }}
*/
var BitField;
/**
* @typedef {Object.<BitField>}
*/
var BitFields;
class Usr {
/**
* binarySearch(a, v, fnCompare)
*
* @param {Array} a is an array
* @param {number|string|Array|Object} v
* @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare]
* @return {number} the index of matching entry if non-negative, otherwise the index of the insertion point
*/
static binarySearch(a, v, fnCompare)
{
var left = 0;
var right = a.length;
var found = 0;
if (fnCompare === undefined) {
fnCompare = function(a, b)
{
return a > b ? 1 : a < b ? -1 : 0;
};
}
while (left < right) {
var middle = (left + right) >> 1;
var compareResult;
compareResult = fnCompare(v, a[middle]);
if (compareResult > 0) {
left = middle + 1;
} else {
right = middle;
found = !compareResult;
}
}
return found ? left : ~left;
}
/**
* binaryInsert(a, v, fnCompare)
*
* If element v already exists in array a, the array is unchanged (we don't allow duplicates); otherwise, the
* element is inserted into the array at the appropriate index.
*
* @param {Array} a is an array
* @param {number|string|Array|Object} v is the value to insert
* @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare]
*/
static binaryInsert(a, v, fnCompare)
{
var index = Usr.binarySearch(a, v, fnCompare);
if (index < 0) {
a.splice(-(index + 1), 0, v);
}
}
/**
* getTimestamp()
*
* @return {string} timestamp containing the current date and time ("yyyy-mm-dd hh:mm:ss")
*/
static getTimestamp()
{
var date = new Date();
var padNum = function(n)
{
return (n < 10 ? "0" : "") + n;
};
return date.getFullYear() + "-" + padNum(date.getMonth() + 1) + "-" + padNum(date.getDate()) + " " + padNum(date.getHours()) + ":" + padNum(date.getMinutes()) + ":" + padNum(date.getSeconds());
}
/**
* getMonthDays(nMonth, nYear)
*
* Note that if we're being called on behalf of the RTC, its year is always truncated to two digits (mod 100),
* so we have no idea what century the year 0 might refer to. When using the normal leap-year formula, 0 fails
* the mod 100 test but passes the mod 400 test, so as far as the RTC is concerned, every century year is a leap
* year. Since we're most likely dealing with the year 2000, that's fine, since 2000 was also a leap year.
*
* TODO: There IS a separate CMOS byte that's supposed to be set to CMOS_ADDR.CENTURY_DATE; it's always BCD,
* so theoretically it will contain values like 0x19 or 0x20 (for the 20th and 21st centuries, respectively), and
* we could add that as another parameter to this function, to improve the accuracy, but that would go beyond what
* a real RTC actually does.
*
* @param {number} nMonth (1-12)
* @param {number} nYear (normally a 4-digit year, but it may also be mod 100)
* @return {number} the maximum (1-based) day allowed for the specified month and year
*/
static getMonthDays(nMonth, nYear)
{
var nDays = Usr.aMonthDays[nMonth - 1];
if (nDays == 28) {
if ((nYear % 4) === 0 && ((nYear % 100) || (nYear % 400) === 0)) {
nDays++;
}
}
return nDays;
}
/**
* formatDate(sFormat, date)
*
* @param {string} sFormat (eg, "F j, Y", "Y-m-d H:i:s")
* @param {Date} [date] (default is the current time)
* @return {string}
*
* Supported identifiers in sFormat include:
*
* a: lowercase ante meridiem and post meridiem (am or pm)
* d: day of the month, 2 digits with leading zeros (01,02,...,31)
* D: 3-letter day of the week ("Sun","Mon",...,"Sat")
* F: month ("January","February",...,"December")
* g: hour in 12-hour format, without leading zeros (1,2,...,12)
* h: hour in 24-hour format, without leading zeros (0,1,...,23)
* H: hour in 24-hour format, with leading zeros (00,01,...,23)
* i: minutes, with leading zeros (00,01,...,59)
* j: day of the month, without leading zeros (1,2,...,31)
* l: day of the week ("Sunday","Monday",...,"Saturday")
* m: month, with leading zeros (01,02,...,12)
* M: 3-letter month ("Jan","Feb",...,"Dec")
* n: month, without leading zeros (1,2,...,12)
* s: seconds, with leading zeros (00,01,...,59)
* y: 2-digit year (eg, 14)
* Y: 4-digit year (eg, 2014)
*
* For more inspiration, see: http://php.net/manual/en/function.date.php (of which we support ONLY a subset).
*/
static formatDate(sFormat, date)
{
var sDate = "";
if (!date) date = new Date();
var iHour = date.getHours();
var iDay = date.getDate();
var iMonth = date.getMonth() + 1;
for (var i = 0; i < sFormat.length; i++) {
var ch;
switch ((ch = sFormat.charAt(i))) {
case 'a':
sDate += (iHour < 12 ? "am" : "pm");
break;
case 'd':
sDate += ('0' + iDay).slice(-2);
break;
case 'D':
sDate += Usr.asDays[date.getDay()].substr(0, 3);
break;
case 'F':
sDate += Usr.asMonths[iMonth - 1];
break;
case 'g':
sDate += (!iHour ? 12 : (iHour > 12 ? iHour - 12 : iHour));
break;
case 'h':
sDate += iHour;
break;
case 'H':
sDate += ('0' + iHour).slice(-2);
break;
case 'i':
sDate += ('0' + date.getMinutes()).slice(-2);
break;
case 'j':
sDate += iDay;
break;
case 'l':
sDate += Usr.asDays[date.getDay()];
break;
case 'm':
sDate += ('0' + iMonth).slice(-2);
break;
case 'M':
sDate += Usr.asMonths[iMonth - 1].substr(0, 3);
break;
case 'n':
sDate += iMonth;
break;
case 's':
sDate += ('0' + date.getSeconds()).slice(-2);
break;
case 'y':
sDate += ("" + date.getFullYear()).slice(-2);
break;
case 'Y':
sDate += date.getFullYear();
break;
default:
sDate += ch;
break;
}
}
return sDate;
}
/**
* defineBitFields(bfs)
*
* Prepares a bit field definition for use with getBitField() and setBitField(); eg:
*
* var bfs = Usr.defineBitFields({num:20, count:8, btmod:1, type:3});
*
* The above defines a set of bit fields containing four fields: num (bits 0-19), count (bits 20-27), btmod (bit 28), and type (bits 29-31).
*
* Usr.setBitField(bfs.num, n, 1);
*
* The above set bit field "bfs.num" in numeric variable "n" to the value 1.
*
* @param {Object} bfs
* @return {BitFields}
*/
static defineBitFields(bfs)
{
var bit = 0;
for (var f in bfs) {
var width = bfs[f];
var mask = ((1 << width) - 1) << bit;
bfs[f] = {mask: mask, shift: bit};
bit += width;
}
return bfs;
}
/**
* initBitFields(bfs, ...)
*
* @param {BitFields} bfs
* @param {...number} var_args
* @return {number} a value containing all supplied bit fields
*/
static initBitFields(bfs, var_args)
{
var v = 0, i = 1;
for (var f in bfs) {
if (i >= arguments.length) break;
v = Usr.setBitField(bfs[f], v, arguments[i++]);
}
return v;
}
/**
* getBitField(bf, v)
*
* @param {BitField} bf
* @param {number} v is a value containing bit fields
* @return {number} the value of the bit field in v defined by bf
*/
static getBitField(bf, v)
{
return (v & bf.mask) >> bf.shift;
}
/**
* setBitField(bf, v, n)
*
* @param {BitField} bf
* @param {number} v is a value containing bit fields
* @param {number} n is a value to store in v in the bit field defined by bf
* @return {number} updated v
*/
static setBitField(bf, v, n)
{
return (v & ~bf.mask) | ((n << bf.shift) & bf.mask);
}
/**
* indexOf(a, t, i)
*
* Use this instead of Array.prototype.indexOf() if you can't be sure the browser supports it.
*
* @param {Array} a
* @param {*} t
* @param {number} [i]
* @returns {number}
*/
static indexOf(a, t, i)
{
if (Array.prototype.indexOf) {
return a.indexOf(t, i);
}
i = i || 0;
if (i < 0) i += a.length;
if (i < 0) i = 0;
for (var n = a.length; i < n; i++) {
if (i in a && a[i] === t) return i;
}
return -1;
}
}
Usr.asDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
Usr.asMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
Usr.aMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
/**
* getTime()
*
* @return {number} the current time, in milliseconds
*/
Usr.getTime = Date.now || function() { return +new Date(); };
/**
* @copyright http://pcjs.org/modules/shared/lib/weblib.js (C) Jeff Parsons 2012-2017
*/
/*
* According to http://www.w3schools.com/jsref/jsref_obj_global.asp, these are the *global* properties
* and functions of JavaScript-in-the-Browser:
*
* Property Description
* ---
* Infinity A numeric value that represents positive/negative infinity
* NaN "Not-a-Number" value
* undefined Indicates that a variable has not been assigned a value
*
* Function Description
* ---
* decodeURI() Decodes a URI
* decodeURIComponent() Decodes a URI component
* encodeURI() Encodes a URI
* encodeURIComponent() Encodes a URI component
* escape() Deprecated in version 1.5. Use encodeURI() or encodeURIComponent() instead
* eval() Evaluates a string and executes it as if it was script code
* isFinite() Determines whether a value is a finite, legal number
* isNaN() Determines whether a value is an illegal number
* Number() Converts an object's value to a number
* parseFloat() Parses a string and returns a floating point number
* parseInt() Parses a string and returns an integer
* String() Converts an object's value to a string
* unescape() Deprecated in version 1.5. Use decodeURI() or decodeURIComponent() instead
*
* And according to http://www.w3schools.com/jsref/obj_window.asp, these are the properties and functions
* of the *window* object.
*
* Property Description
* ---
* closed Returns a Boolean value indicating whether a window has been closed or not
* defaultStatus Sets or returns the default text in the statusbar of a window
* document Returns the Document object for the window (See Document object)
* frames Returns an array of all the frames (including iframes) in the current window
* history Returns the History object for the window (See History object)
* innerHeight Returns the inner height of a window's content area
* innerWidth Returns the inner width of a window's content area
* length Returns the number of frames (including iframes) in a window
* location Returns the Location object for the window (See Location object)
* name Sets or returns the name of a window
* navigator Returns the Navigator object for the window (See Navigator object)
* opener Returns a reference to the window that created the window
* outerHeight Returns the outer height of a window, including toolbars/scrollbars
* outerWidth Returns the outer width of a window, including toolbars/scrollbars
* pageXOffset Returns the pixels the current document has been scrolled (horizontally) from the upper left corner of the window
* pageYOffset Returns the pixels the current document has been scrolled (vertically) from the upper left corner of the window
* parent Returns the parent window of the current window
* screen Returns the Screen object for the window (See Screen object)
* screenLeft Returns the x coordinate of the window relative to the screen
* screenTop Returns the y coordinate of the window relative to the screen
* screenX Returns the x coordinate of the window relative to the screen
* screenY Returns the y coordinate of the window relative to the screen
* self Returns the current window
* status Sets or returns the text in the statusbar of a window
* top Returns the topmost browser window
*
* Method Description
* ---
* alert() Displays an alert box with a message and an OK button
* atob() Decodes a base-64 encoded string
* blur() Removes focus from the current window
* btoa() Encodes a string in base-64
* clearInterval() Clears a timer set with setInterval()
* clearTimeout() Clears a timer set with setTimeout()
* close() Closes the current window
* confirm() Displays a dialog box with a message and an OK and a Cancel button
* createPopup() Creates a pop-up window
* focus() Sets focus to the current window
* moveBy() Moves a window relative to its current position
* moveTo() Moves a window to the specified position
* open() Opens a new browser window
* print() Prints the content of the current window
* prompt() Displays a dialog box that prompts the visitor for input
* resizeBy() Resizes the window by the specified pixels
* resizeTo() Resizes the window to the specified width and height
* scroll() This method has been replaced by the scrollTo() method.
* scrollBy() Scrolls the content by the specified number of pixels
* scrollTo() Scrolls the content to the specified coordinates
* setInterval() Calls a function or evaluates an expression at specified intervals (in milliseconds)
* setTimeout() Calls a function or evaluates an expression after a specified number of milliseconds
* stop() Stops the window from loading
*/
class Web {
/**
* log(s, type)
*
* For diagnostic output only. DEBUG must be true (or "--debug" specified via the command-line)
* for Component.log() to display anything.
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
static log(s, type)
{
Component.log(s, type);
}
/**
* notice(s, fPrintOnly, id)
*
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
*/
static notice(s, fPrintOnly, id)
{
Component.notice(s, fPrintOnly, id);
}
/**
* getResource(sURL, dataPost, fAsync, done, progress)
*
* Request the specified resource (sURL), and once the request is complete, notify done().
*
* If fAsync is true, a done() callback should ALWAYS be supplied; otherwise, you'll have no
* idea when the request is complete or what the response was. done() is passed three parameters:
*
* done(sURL, sResource, nErrorCode)
*
* If nErrorCode is zero, sResource should contain the requested data; otherwise, an error occurred.
*
* If dataPost is set to a string, that string can be used to control the response format;
* by default, the response format is plain text, but you can specify "bytes" to request arbitrary
* binary data, which should come back as a string of bytes.
*
* TODO: The "bytes" option works by calling overrideMimeType(), which was never a best practice.
* Instead, we should implement supported response types ("text" and "arraybuffer", at a minimum)
* by setting xmlHTTP.responseType to one of those values before calling xmlHTTP.send().
*
* @param {string} sURL
* @param {string|Object|null} [dataPost] for a POST request (default is a GET request)
* @param {boolean} [fAsync] is true for an asynchronous request
* @param {function(string,string,number)} [done]
* @param {function(number)} [progress]
* @return {Array|null} Array containing [sResource, nErrorCode], or null if no response yet
*/
static getResource(sURL, dataPost, fAsync = false, done, progress)
{
var nErrorCode = 0, sResource = null, response = null;
if (typeof resources == 'object' && (sResource = resources[sURL])) {
if (done) done(sURL, sResource, nErrorCode);
return [sResource, nErrorCode];
}
else if (fAsync && typeof resources == 'function') {
resources(sURL, function(sResource, nErrorCode)
{
if (done) done(sURL, sResource, nErrorCode);
});
return response;
}
if (DEBUG) {
/*
* The larger resources we put on archive.pcjs.org should also be available locally...
*/
sURL = sURL.replace(/^http:\/\/archive.pcjs.org(\/.*)\/([^\/]*)$/, "$1/archive/$2");
}
var xmlHTTP = (window.XMLHttpRequest? new window.XMLHttpRequest() : new window.ActiveXObject("Microsoft.XMLHTTP"));
if (fAsync) {
xmlHTTP.onreadystatechange = function()
{
if (xmlHTTP.readyState !== 4) {
if (progress) progress(1);
return;
}
/*
* The following line was recommended for WebKit, as a work-around to prevent the handler firing multiple
* times when debugging. Unfortunately, that's not the only XMLHttpRequest problem that occurs when
* debugging, so I think the WebKit problem is deeper than that. When we have multiple XMLHttpRequests
* pending, any debugging activity means most of them simply get dropped on floor, so what may actually be
* happening are mis-notifications rather than redundant notifications.
*
* xmlHTTP.onreadystatechange = undefined;
*/
sResource = xmlHTTP.responseText;
/*
* The normal "success" case is an HTTP status code of 200, but when testing with files loaded
* from the local file system (ie, when using the "file:" protocol), we have to be a bit more "flexible".
*/
if (xmlHTTP.status == 200 || !xmlHTTP.status && sResource.length && Web.getHostProtocol() == "file:") {
if (MAXDEBUG) Web.log("xmlHTTP.onreadystatechange(" + sURL + "): returned " + sResource.length + " bytes");
}
else {
nErrorCode = xmlHTTP.status || -1;
Web.log("xmlHTTP.onreadystatechange(" + sURL + "): error code " + nErrorCode);
}
if (progress) progress(2);
if (done) done(sURL, sResource, nErrorCode);
};
}
if (progress) progress(0);
if (dataPost && typeof dataPost == "object") {
var sDataPost = "";
for (var p in dataPost) {
if (!dataPost.hasOwnProperty(p)) continue;
if (sDataPost) sDataPost += "&";
sDataPost += p + '=' + encodeURIComponent(dataPost[p]);
}
sDataPost = sDataPost.replace(/%20/g, '+');
if (MAXDEBUG) Web.log("Web.getResource(POST " + sURL + "): " + sDataPost.length + " bytes");
xmlHTTP.open("POST", sURL, fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it)
xmlHTTP.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xmlHTTP.send(sDataPost);
} else {
if (MAXDEBUG) Web.log("Web.getResource(GET " + sURL + ")");
xmlHTTP.open("GET", sURL, fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it)
if (dataPost == "bytes") {
xmlHTTP.overrideMimeType("text/plain; charset=x-user-defined");
}
xmlHTTP.send();
}
if (!fAsync) {
sResource = xmlHTTP.responseText;
if (xmlHTTP.status == 200) {
if (MAXDEBUG) Web.log("Web.getResource(" + sURL + "): returned " + sResource.length + " bytes");
} else {
nErrorCode = xmlHTTP.status || -1;
Web.log("Web.getResource(" + sURL + "): error code " + nErrorCode);
}
if (done) done(sURL, sResource, nErrorCode);
response = [sResource, nErrorCode];
}
return response;
}
/**
* parseMemoryResource(sURL, sData)
*
* This converts a variety of JSON-style data streams into an Object with the following properties:
*
* aBytes
* aSymbols
* addrLoad
* addrExec
*
* If the source data contains a 'bytes' array, it's passed through to 'aBytes'; alternatively, if
* it contains a 'words' array, the values are converted from 16-bit to 8-bit and stored in 'aBytes',
* and if it contains a 'longs' array, the values are converted from 32-bit longs into bytes and
* stored in 'aBytes'.
*
* Alternatively, if the source data contains a 'data' array, we simply pass that through to the output
* object as:
*
* aData
*
* @param {string} sURL
* @param {string} sData
* @return {Object|null} (resource)
*/
static parseMemoryResource(sURL, sData)
{
var i;
var resource = {
aBytes: null,
aSymbols: null,
addrLoad: null,
addrExec: null
};
if (sData.charAt(0) == "[" || sData.charAt(0) == "{") {
try {
var a, ib, data;
if (sData.substr(0, 1) == "<") { // if the "data" begins with a "<"...
/*
* Early server configs reported an error (via the nErrorCode parameter) if a tape URL was invalid,
* but more recent server configs now display a somewhat friendlier HTML error page. The downside,
* however, is that the original error has been buried, and we've received "data" that isn't actually
* tape data. So if the data we've received appears to be "HTML-like", we treat it as an error message.
*/
throw new Error(sData);
}
/*
* TODO: IE9 is rather unfriendly and restrictive with regard to how much data it's willing to
* eval(). In particular, the 10Mb disk image we use for the Windows 1.01 demo config fails in
* IE9 with an "Out of memory" exception. One work-around would be to chop the data into chunks
* (perhaps one track per chunk, using regular expressions) and then manually re-assemble it.
*
* However, it turns out that using JSON.parse(sDiskData) instead of eval("(" + sDiskData + ")")
* is a much easier fix. The only drawback is that we must first quote any unquoted property names
* and remove any comments, because while eval() was cool with them, JSON.parse() is more particular;
* the following RegExp replacements take care of those requirements.
*
* The use of hex values is something else that eval() was OK with, but JSON.parse() is not, and
* while I've stopped using hex values in DumpAPI responses (at least when "format=json" is specified),
* I can't guarantee they won't show up in "legacy" images, and there's no simple RegExp replacement
* for transforming hex values into decimal values, so I cop out and fall back to eval() if I detect
* any hex prefixes ("0x") in the sequence. Ditto for error messages, which appear like so:
*
* ["unrecognized disk path: test.img"]
*/
if (sData.indexOf("0x") < 0 && sData.indexOf("0o") < 0 && sData.substr(0, 2) != '["') {
data = JSON.parse(sData.replace(/([a-z]+):/gm, '"$1":').replace(/\/\/[^\n]*/gm, ""));
} else {
data = eval("(" + sData + ")");
}
resource.addrLoad = data['load'];
resource.addrExec = data['exec'];
if (a = data['bytes']) {
resource.aBytes = a;
}
else if (a = data['words']) {
/*
* Convert all words into bytes
*/
resource.aBytes = new Array(a.length * 2);
for (i = 0, ib = 0; i < a.length; i++) {
resource.aBytes[ib++] = a[i] & 0xff;
resource.aBytes[ib++] = (a[i] >> 8) & 0xff;
}
}
else if (a = data['longs']) {
/*
* Convert all dwords (longs) into bytes
*/
resource.aBytes = new Array(a.length * 4);
for (i = 0, ib = 0; i < a.length; i++) {
resource.aBytes[ib++] = a[i] & 0xff;
resource.aBytes[ib++] = (a[i] >> 8) & 0xff;
resource.aBytes[ib++] = (a[i] >> 16) & 0xff;
resource.aBytes[ib++] = (a[i] >> 24) & 0xff;
}
}
else if (a = data['data']) {
resource.aData = a;
}
else {
resource.aBytes = data;
}
if (resource.aBytes) {
if (!resource.aBytes.length) {
Component.error("Empty resource: " + sURL);
resource = null;
}
else if (resource.aBytes.length == 1) {
Component.error(resource.aBytes[0]);
resource = null;
}
}
resource.aSymbols = data['symbols'];
} catch (e) {
Component.error("Resource data error (" + sURL + "): " + e.message);
resource = null;
}
}
else {
/*
* Parse the data manually; we assume it's a series of hex byte-values separated by whitespace.
*/
var ab = [];
var sHexData = sData.replace(/\n/gm, " ").replace(/ +$/, "");
var asHexData = sHexData.split(" ");
for (i = 0; i < asHexData.length; i++) {
var n = parseInt(asHexData[i], 16);
if (isNaN(n)) {
Component.error("Resource data error (" + sURL + "): invalid hex byte (" + asHexData[i] + ")");
break;
}
ab.push(n & 0xff);
}
if (i == asHexData.length) resource.aBytes = ab;
}
return resource;
}
/**
* sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName)
*
* Send a report (eg, bug report) to the server.
*
* @param {string} sApp (eg, "PCjs")
* @param {string} sVer (eg, "1.02")
* @param {string} sURL (eg, "/devices/pc/machine/5150/mda/64kb/machine.xml")
* @param {string} sUser (ie, the user key, if any)
* @param {string} sType (eg, "bug"); one of ReportAPI.TYPE.*
* @param {string} sReport (eg, unparsed state data)
* @param {string} [sHostName] (default is http://SITEHOST)
*/
static sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName)
{
var dataPost = {};
dataPost[ReportAPI.QUERY.APP] = sApp;
dataPost[ReportAPI.QUERY.VER] = sVer;
dataPost[ReportAPI.QUERY.URL] = sURL;
dataPost[ReportAPI.QUERY.USER] = sUser;
dataPost[ReportAPI.QUERY.TYPE] = sType;
dataPost[ReportAPI.QUERY.DATA] = sReport;
var sReportURL = (sHostName? sHostName : "http://" + SITEHOST) + ReportAPI.ENDPOINT;
Web.getResource(sReportURL, dataPost, true);
}
/**
* getHost()
*
* @return {string}
*/
static getHost()
{
return ("http://" + (window? window.location.host : SITEHOST));
}
/**
* getHostURL()
*
* @return {string|null}
*/
static getHostURL()
{
return (window? window.location.href : null);
}
/**
* getHostProtocol()
*
* @return {string}
*/
static getHostProtocol()
{
return (window? window.location.protocol : "file:");
}
/**
* getUserAgent()
*
* @return {string}
*/
static getUserAgent()
{
return (window? window.navigator.userAgent : "");
}
/**
* hasLocalStorage
*
* true if localStorage support exists, is enabled, and works; false otherwise
*
* @return {boolean}
*/
static hasLocalStorage()
{
if (Web.fLocalStorage == null) {
var f = false;
if (window) {
try {
window.localStorage.setItem(Web.sLocalStorageTest, Web.sLocalStorageTest);
f = (window.localStorage.getItem(Web.sLocalStorageTest) == Web.sLocalStorageTest);
window.localStorage.removeItem(Web.sLocalStorageTest);
} catch (e) {
Web.logLocalStorageError(e);
f = false;
}
}
Web.fLocalStorage = f;
}
return Web.fLocalStorage;
}
/**
* logLocalStorageError(e)
*
* @param {Error} e is an exception
*/
static logLocalStorageError(e)
{
Web.log(e.message, "localStorage error");
}
/**
* getLocalStorageItem(sKey)
*
* Returns the requested key value, or null if the key does not exist, or undefined if localStorage is not available
*
* @param {string} sKey
* @return {string|null|undefined} sValue
*/
static getLocalStorageItem(sKey)
{
var sValue;
if (window) {
try {
sValue = window.localStorage.getItem(sKey);
} catch (e) {
Web.logLocalStorageError(e);
}
}
return sValue;
}
/**
* setLocalStorageItem(sKey, sValue)
*
* @param {string} sKey
* @param {string} sValue
* @return {boolean} true if localStorage is available, false if not
*/
static setLocalStorageItem(sKey, sValue)
{
try {
window.localStorage.setItem(sKey, sValue);
return true;
} catch (e) {
Web.logLocalStorageError(e);
}
return false;
}
/**
* removeLocalStorageItem(sKey)
*
* @param {string} sKey
*/
static removeLocalStorageItem(sKey)
{
try {
window.localStorage.removeItem(sKey);
} catch (e) {
Web.logLocalStorageError(e);
}
}
/**
* getLocalStorageKeys()
*
* @return {Array}
*/
static getLocalStorageKeys()
{
var a = [];
try {
for (var i = 0, c = window.localStorage.length; i < c; i++) {
a.push(window.localStorage.key(i));
}
} catch (e) {
Web.logLocalStorageError(e);
}
return a;
}
/**
* reloadPage()
*/
static reloadPage()
{
if (window) window.location.reload();
}
/**
* isUserAgent(s)
*
* Check the browser's user-agent string for the given substring; "iOS" and "MSIE" are special values you can
* use that will match any iOS or MSIE browser, respectively (even IE11, in the case of "MSIE").
*
* 2013-11-06: In a questionable move, MSFT changed the user-agent reported by IE11 on Windows 8.1, eliminating
* the "MSIE" string (which MSDN calls a "version token"; see http://msdn.microsoft.com/library/ms537503.aspx);
* they say "public websites should rely on feature detection, rather than browser detection, in order to design
* their sites for browsers that don't support the features used by the website." So, in IE11, we get a user-agent
* that tries to fool apps into thinking the browser is more like WebKit or Gecko:
*
* Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko
*
* That's a nice idea, but in the meantime, they hosed the XSL transform code in embed.js, which contained
* some very critical browser-specific code; turning on IE's "Compatibility Mode" didn't help either, because
* that's a sledgehammer solution which restores the old user-agent string but also disables other features like
* HTML5 canvas support. As an interim solution, I'm treating any "MSIE" check as a check for either "MSIE" or
* "Trident".
*
* UPDATE: I've since found ways to make the code in embed.js more browser-agnostic, so for now, there's isn't
* any code that cares about "MSIE", but I've left the change in place, because I wouldn't be surprised if I'll
* need more IE-specific code in the future, perhaps for things like copy/paste functionality, or mouse capture.
*
* @param {string} s is a substring to search for in the user-agent; as noted above, "iOS" and "MSIE" are special values
* @return {boolean} is true if the string was found, false if not
*/
static isUserAgent(s)
{
if (window) {
var userAgent = Web.getUserAgent();
/*
* Here's one case where we have to be careful with Component, because when isUserAgent() is called by
* the init code below, component.js hasn't been loaded yet. The simple solution for now is to remove the call.
*
* Web.log("agent: " + userAgent);
*
* And yes, it would be pointless to use the conditional (?) operator below, if not for the Google Closure
* Compiler (v20130823) failing to detect the entire expression as a boolean.
*/
return s == "iOS" && !!userAgent.match(/(iPod|iPhone|iPad)/) && !!userAgent.match(/AppleWebKit/) || s == "MSIE" && !!userAgent.match(/(MSIE|Trident)/) || (userAgent.indexOf(s) >= 0);
}
return false;
}
/**
* isMobile()
*
* Check the browser's user-agent string for the substring "Mobi", as per Mozilla recommendation:
*
* https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent
*
* @return {boolean} is true if the browser appears to be a mobile (ie, non-desktop) web browser, false if not
*/
static isMobile()
{
return Web.isUserAgent("Mobi");
}
/**
* getURLParm(sParm)
*
* First looks for sParm exactly as specified, then looks for the lower-case version.
*
* @param {string} sParm
* @return {string|undefined}
*/
static getURLParm(sParm)
{
if (!Web.parmsURL) {
Web.parmsURL = Web.parseURLParms();
}
return Web.parmsURL[sParm] || Web.parmsURL[sParm.toLowerCase()];
}
/**
* parseURLParms(sParms)
*
* @param {string} [sParms] containing the parameter portion of a URL (ie, after the '?')
* @return {Object} containing properties for each parameter found
*/
static parseURLParms(sParms)
{
var aParms = {};
if (window) { // an alternative to "if (typeof module === 'undefined')" if require("defines") was used
if (!sParms) {
/*
* Note that window.location.href returns the entire URL, whereas window.location.search
* returns only the parameters, if any (starting with the '?', which we skip over with a substr() call).
*/
sParms = window.location.search.substr(1);
}
var match;
var pl = /\+/g; // RegExp for replacing addition symbol with a space
var search = /([^&=]+)=?([^&]*)/g;
var decode = function(s)
{
return decodeURIComponent(s.replace(pl, " "));
};
while ((match = search.exec(sParms))) {
aParms[decode(match[1])] = decode(match[2]);
}
}
return aParms;
}
/**
* downloadFile(sData, sType, fBase64, sFileName)
*
* @param {string} sData
* @param {string} sType
* @param {boolean} [fBase64]
* @param {string} [sFileName]
*/
static downloadFile(sData, sType, fBase64, sFileName)
{
var link = null, sAlert;
var sURI = "data:application/" + sType + (fBase64? ";base64" : "") + ",";
if (!Web.isUserAgent("Firefox")) {
sURI += (fBase64? sData : encodeURI(sData));
} else {
sURI += (fBase64? sData : encodeURIComponent(sData));
}
if (sFileName) {
link = document.createElement('a');
if (typeof link.download != 'string') link = null;
}
if (link) {
link.href = sURI;
link.download = sFileName;
document.body.appendChild(link); // Firefox allegedly requires the link to be in the body
link.click();
document.body.removeChild(link);
sAlert = 'Check your Downloads folder for ' + sFileName + '.';
} else {
window.open(sURI);
sAlert = 'Check your browser for a new window/tab containing the requested data' + (sFileName? (' (' + sFileName + ')') : '') + '.';
}
return sAlert;
}
/**
* onCountRepeat(n, fnRepeat, fnComplete, msDelay)
*
* Call fnRepeat() n times with an msDelay millisecond delay between calls,
* then call fnComplete() when n has been exhausted OR fnRepeat() returns false.
*
* @param {number} n
* @param {function()} fnRepeat
* @param {function()} fnComplete
* @param {number} [msDelay]
*/
static onCountRepeat(n, fnRepeat, fnComplete, msDelay)
{
var fnTimeout = function doCountRepeat()
{
n -= 1;
if (n >= 0) {
if (!fnRepeat()) n = 0;
}
if (n > 0) {
setTimeout(fnTimeout, msDelay || 0);
return;
}
fnComplete();
};
fnTimeout();
}
/**
* onClickRepeat(e, msDelay, msRepeat, fn)
*
* Repeatedly call fn() with an initial msDelay, and an msRepeat delay thereafter,
* as long as HTML control Object e has an active "down" event and fn() returns true.
*
* @param {Object} e
* @param {number} msDelay
* @param {number} msRepeat
* @param {function(boolean)} fn is passed false on the first call, true on all repeated calls
*/
static onClickRepeat(e, msDelay, msRepeat, fn)
{
var ms = 0, timer = null, fIgnoreMouseEvents = false;
var fnRepeat = function doClickRepeat()
{
if (fn(ms === msRepeat)) {
timer = setTimeout(fnRepeat, ms);
ms = msRepeat;
}
};
e.onmousedown = function()
{
// Web.log("onMouseDown()");
if (!fIgnoreMouseEvents) {
if (!timer) {
ms = msDelay;
fnRepeat();
}
}
};
e.ontouchstart = function()
{
// Web.log("onTouchStart()");
if (!timer) {
ms = msDelay;
fnRepeat();
}
};
e.onmouseup = e.onmouseout = function()
{
// Web.log("onMouseUp()/onMouseOut()");
if (timer) {
clearTimeout(timer);
timer = null;
}
};
e.ontouchend = e.ontouchcancel = function()
{
// Web.log("onTouchEnd()/onTouchCancel()");
if (timer) {
clearTimeout(timer);
timer = null;
}
/*
* Devices that generate ontouch* events ALSO generate onmouse* events,
* and generally do so immediately after all the touch events are complete,
* so unless we want double the action, we need to ignore mouse events.
*/
fIgnoreMouseEvents = true;
};
}
/**
* onPageEvent(sName, fn)
*
* For 'onload', 'onunload', and 'onpageshow' events, most callers should NOT use this function, but
* instead use Web.onInit(), Web.onShow(), and Web.onExit(), respectively.
*
* The only components that should still use onPageEvent() are THIS component (see the bottom of this file)
* and components that need to capture other events (eg, the 'onresize' event in the Video component).
*
* This function creates a chain of callbacks, allowing multiple JavaScript modules to define handlers
* for the same event, which wouldn't be possible if everyone modified window['onload'], window['onunload'],
* etc, themselves. However, that's less of a concern now, because assuming everyone else is now using
* onInit(), onExit(), etc, then there really IS only one component setting the window callback: this one.
*
* NOTE: It's risky to refer to obscure event handlers with "dot" names, because the Closure Compiler may
* erroneously replace them (eg, window.onpageshow is a good example).
*
* @param {string} sFunc
* @param {function()} fn
*/
static onPageEvent(sFunc, fn)
{
if (window) {
var fnPrev = window[sFunc];
if (typeof fnPrev !== 'function') {
window[sFunc] = fn;
} else {
/*
* TODO: Determine whether there's any value in receiving/sending the Event object that the
* browser provides when it generates the original event.
*/
window[sFunc] = function onWindowEvent()
{
if (fnPrev) fnPrev();
fn();
};
}
}
};
/**
* onInit(fn)
*
* Use this instead of setting window.onload. Allows multiple JavaScript modules to define their own 'onload' event handler.
*
* @param {function()} fn
*/
static onInit(fn)
{
Web.aPageEventHandlers['init'].push(fn);
};
/**
* onShow(fn)
*
* @param {function()} fn
*
* Use this instead of setting window.onpageshow. Allows multiple JavaScript modules to define their own 'onpageshow' event handler.
*/
static onShow(fn)
{
Web.aPageEventHandlers['show'].push(fn);
};
/**
* onExit(fn)
*
* @param {function()} fn
*
* Use this instead of setting window.onunload. Allows multiple JavaScript modules to define their own 'onunload' event handler.
*/
static onExit(fn)
{
Web.aPageEventHandlers['exit'].push(fn);
};
/**
* doPageEvent(afn)
*
* @param {Array.<function()>} afn
*/
static doPageEvent(afn)
{
if (Web.fPageEventsEnabled) {
try {
for (var i = 0; i < afn.length; i++) {
afn[i]();
}
} catch (e) {
Web.notice("An unexpected exception occurred:\n\n" + e.message + "\n\nPlease send this information to support@pcjs.org. Thanks.");
}
}
};
/**
* enablePageEvents(fEnable)
*
* @param {boolean} fEnable is true to enable page events, false to disable (they're enabled by default)
*/
static enablePageEvents(fEnable)
{
if (!Web.fPageEventsEnabled && fEnable) {
Web.fPageEventsEnabled = true;
if (Web.fPageLoaded) Web.sendPageEvent('init');
if (Web.fPageShowed) Web.sendPageEvent('show');
return;
}
Web.fPageEventsEnabled = fEnable;
}
/**
* sendPageEvent(sEvent)
*
* This allows us to manually trigger page events.
*
* @param {string} sEvent (one of 'init', 'show' or 'exit')
*/
static sendPageEvent(sEvent)
{
if (Web.aPageEventHandlers[sEvent]) {
Web.doPageEvent(Web.aPageEventHandlers[sEvent]);
}
}
}
Web.parmsURL = null; // initialized on first call to parseURLParms()
Web.aPageEventHandlers = {
'init': [], // list of window 'onload' handlers
'show': [], // list of window 'onpageshow' handlers
'exit': [] // list of window 'onunload' handlers (although we prefer to use 'onbeforeunload' if possible)
};
Web.fPageLoaded = false; // set once the page's first 'onload' event has occurred
Web.fPageShowed = false; // set once the page's first 'onpageshow' event has occurred
Web.fPageEventsEnabled = true; // default is true, set to false (or true) by enablePageEvents()
/**
* fLocalStorage
*
* true if localStorage support exists, is enabled, and works; "falsey" otherwise
*
* @type {boolean|null}
*/
Web.fLocalStorage = null;
/**
* TODO: Is there any way to get the Closure Compiler to stop inlining this string? This isn't cutting it.
*
* @const {string}
*/
Web.sLocalStorageTest = "PCjs.localStorage";
Web.onPageEvent('onload', function onPageLoad() {
Web.fPageLoaded = true;
Web.doPageEvent(Web.aPageEventHandlers['init']);
});
Web.onPageEvent('onpageshow', function onPageShow() {
Web.fPageShowed = true;
Web.doPageEvent(Web.aPageEventHandlers['show']);
});
Web.onPageEvent(Web.isUserAgent("iOS")? 'onpagehide' : (Web.isUserAgent("Opera")? 'onunload' : 'onbeforeunload'), function onPageUnload() {
Web.doPageEvent(Web.aPageEventHandlers['exit']);
});
/**
* @copyright http://pcjs.org/modules/shared/lib/component.js (C) Jeff Parsons 2012-2017
*/
/*
* All PCjs components now use JSDoc types, primarily so that Google's Closure Compiler will compile
* everything with zero warnings when ADVANCED_OPTIMIZATIONS are enabled. For more information about
* the JSDoc types supported by the Closure Compiler:
*
* https://developers.google.com/closure/compiler/docs/js-for-compiler#types
*
* I also attempted to validate this code with JSLint, but it complained too much; eg, it didn't like
* "while (true)", a tried and "true" programming convention for decades, and it wanted me to replace
* all "++" and "--" operators with "+= 1" and "-= 1", use "(s || '')" instead of "(s? s : '')", etc.
*
* I prefer sticking with traditional C-style idioms, in part because they are more portable. That
* does NOT mean I'm trying to write "portable JavaScript," but some of this code was ported from C code
* I'd written long ago, so portability is good, and I'm not going to throw that away if there's no need.
*
* UPDATE: I've since switched from JSLint to JSHint, which seems to have more reasonable defaults.
* And for new code, I have adopted some popular JavaScript idioms, like "(s || '')", although the need
* for those kinds of expressions will be reduced as I also start adopting some ES6 features, like
* default parameters.
*/
/**
* 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
* subclass (eg, SerialPort), 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 Component {
/**
* Component(type, parms, bitsMessage)
*
* A Component object requires:
*
* type: a user-defined type name (eg, "CPU")
*
* and accepts any or all of the following (parms) properties:
*
* id: component ID (default is "")
* name: component name (default is ""; if blank, toString() will use the type name only)
* comment: component comment string (default is undefined)
*
* Component subclasses will usually have additional (parms) properties.
*
* @param {string} type
* @param {Object} [parms]
* @param {number} [bitsMessage] selects message(s) that the component wants to enable (default is 0)
*/
constructor(type, parms, bitsMessage)
{
this.type = type;
if (!parms) parms = {'id': "", 'name': ""};
this.id = parms['id'] || "";
this.name = parms['name'];
this.comment = parms['comment'];
this.parms = parms;
/*
* The following Component properties need to be accessible by other machines and/or command scripts;
* well, OK, or we could have exported some new functions to walk the contents of these properties, as we
* did with findMachineComponent(), but this works just as well.
*
* Also, while the double-assignment looks silly (ie, using both dot and bracket property notation), it
* resolves a complaint from the Closure Compiler, because if we use ONLY bracket notation here, then the
* Compiler wants us to change all the other references to bracket notation as well.
*/
this.exports = this['exports'] = {};
this.bindings = this['bindings'] = {};
var i = this.id.indexOf('.');
if (i < 0) {
this.idComponent = this.id;
} else {
this.idMachine = this.id.substr(0, i);
this.idComponent = this.id.substr(i + 1);
}
/*
* Gather all the various component flags (booleans) into a single "flags" object, and encourage
* subclasses to do the same, to reduce the property clutter we have to wade through while debugging.
*/
this.flags = {
ready: false,
busy: false,
busyCancel: false,
initDone: false,
powered: false,
unloading: false,
error: false
};
this.fnReady = null;
this.clearError();
this.bitsMessage = bitsMessage || 0;
this.cmp = null;
this.bus = null;
this.cpu = null;
this.dbg = null;
/*
* TODO: Consider adding another parameter to the Component() constructor that allows components to tell
* us if they support single or multiple instances per machine. For example, there can be multiple SerialPort
* components per machine, but only one CPU component (some machines also support an FPU, but that component
* is considered separate from the CPU).
*
* It's not critical, but it would help catch machine configuration errors; for example, a machine that mistakenly
* includes two CPU components may, aside from wasting memory, end up with odd side-effects, like unresponsive
* CPU controls.
*/
Component.add(this);
}
/**
* Component.add(component)
*
* @param {Component} component
*/
static add(component)
{
/*
* This just generates a lot of useless noise, handy in the early days, not so much these days....
*
* if (DEBUG) Component.log("Component.add(" + component.type + "," + component.id + ")");
*/
Component.components.push(component);
}
/**
* Component.addMachine(idMachine)
*
* @param {string} idMachine
*/
static addMachine(idMachine)
{
Component.machines[idMachine] = {};
}
/**
* Component.addMachineResource(idMachine, sName, data)
*
* @param {string} idMachine
* @param {string|null} sName (name of the resource)
* @param {*} data
*/
static addMachineResource(idMachine, sName, data)
{
/*
* I used to assert(Component.machines[idMachine]), but when we're running as a Node app, embed.js is not used,
* so addMachine() is never called, so resources do not need to be recorded.
*/
if (Component.machines[idMachine] && sName) {
Component.machines[idMachine][sName] = data;
}
}
/**
* Component.getMachineResources(idMachine)
*
* @param {string} idMachine
* @return {Object|undefined}
*/
static getMachineResources(idMachine)
{
return Component.machines[idMachine];
}
/**
* Component.getTime()
*
* @return {number} the current time, in milliseconds
*/
static getTime()
{
return Date.now() || +new Date();
}
/**
* Component.log(s, type)
*
* For diagnostic output only.
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
static log(s, type)
{
if (!COMPILED) {
if (s) {
var sElapsed = "", sMsg = (type? (type + ": ") : "") + s;
if (typeof Usr != "undefined") {
if (Component.msStart === undefined) {
Component.msStart = Component.getTime();
}
sElapsed = (Component.getTime() - Component.msStart) + "ms: ";
}
if (window && window.console) console.log(sElapsed + sMsg.replace(/\n/g, ' '));
}
}
}
/**
* Component.assert(f, s)
*
* Verifies conditions that must be true (for DEBUG builds only).
*
* The Closure Compiler should automatically remove all references to Component.assert() in non-DEBUG builds.
* TODO: Add a task to the build process that "asserts" there are no instances of "assertion failure" in RELEASE builds.
*
* @param {boolean} f is the expression we are asserting to be true
* @param {string} [s] is description of the assertion on failure
*/
static assert(f, s)
{
if (DEBUG) {
if (!f) {
if (!s) s = "assertion failure";
Component.log(s);
throw new Error(s);
}
}
}
/**
* Component.print(s)
*
* Components that inherit from this class should use this.print(), rather than Component.print(), because
* if a Control Panel is loaded, it will override only the instance method, not the class method (overriding the
* class method would improperly affect any other machines loaded on the same page).
*
* @this {Component}
* @param {string} s
*/
static print(s)
{
if (!COMPILED) {
var i = s.lastIndexOf('\n');
if (i >= 0) {
Component.println(s.substr(0, i));
s = s.substr(i + 1);
}
Component.printBuffer += s;
}
}
/**
* Component.println(s, type, id)
*
* Components that inherit from this class should use this.println(), rather than Component.println(), because
* if a Control Panel is loaded, it will override only the instance method, not the class method (overriding the
* class method would improperly affect any other machines loaded on the same page).
*
* @param {string} [s] is the message text
* @param {string} [type] is the message type
* @param {string} [id] is the caller's ID, if any
*/
static println(s, type, id)
{
if (!COMPILED) {
s = Component.printBuffer + (s || "");
Component.log((id? (id + ": ") : "") + (s? ("\"" + s + "\"") : ""), type);
Component.printBuffer = "";
}
}
/**
* Component.notice(s, fPrintOnly, id)
*
* notice() is like println() but implies a need for user notification, so we alert() as well.
*
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
* @return {boolean}
*/
static notice(s, fPrintOnly, id)
{
if (!COMPILED) {
Component.println(s, Component.TYPE.NOTICE, id);
}
if (!fPrintOnly) Component.alertUser((id? (id + ": ") : "") + s);
return true;
}
/**
* Component.warning(s)
*
* @param {string} s describes the warning
*/
static warning(s)
{
if (!COMPILED) {
Component.println(s, Component.TYPE.WARNING);
}
Component.alertUser(s);
}
/**
* Component.error(s)
*
* @param {string} s describes the error; an alert() is displayed as well
*/
static error(s)
{
if (!COMPILED) {
Component.println(s, Component.TYPE.ERROR);
}
Component.alertUser(s);
}
/**
* Component.alertUser(sMessage)
*
* @param {string} sMessage
*/
static alertUser(sMessage)
{
if (window) {
window.alert(sMessage);
} else {
Component.log(sMessage);
}
};
/**
* Component.confirmUser(sPrompt)
*
* @param {string} sPrompt
* @returns {boolean} true if the user clicked OK, false if Cancel/Close
*/
static confirmUser(sPrompt)
{
var fResponse = false;
if (window) {
fResponse = window.confirm(sPrompt);
}
return fResponse;
}
/**
* Component.promptUser()
*
* @param {string} sPrompt
* @param {string} [sDefault]
* @returns {string|null}
*/
static promptUser(sPrompt, sDefault)
{
var sResponse = null;
if (window) {
sResponse = window.prompt(sPrompt, sDefault === undefined? "" : sDefault);
}
return sResponse;
}
/**
* Component.appendControl(control, sText)
*
* @param {Object} control
* @param {string} sText
*/
static appendControl(control, sText)
{
control.value += sText;
/*
* Prevent the <textarea> from getting too large; otherwise, printing becomes slower and slower.
*/
if (COMPILED) {
sText = control.value;
if (sText.length > 8192) control.value = sText.substr(sText.length - 4096);
}
control.scrollTop = control.scrollHeight;
}
/**
* Component.replaceControl(control, sSearch, sReplace)
*
* @param {Object} control
* @param {string} sSearch
* @param {string} sReplace
*/
static replaceControl(control, sSearch, sReplace)
{
var sText = control.value;
var i = sText.lastIndexOf(sSearch);
if (i < 0) {
sText += sSearch + '\n';
} else {
sText = sText.substr(0, i) + sReplace + sText.substr(i + sSearch.length);
}
/*
* Prevent the <textarea> from getting too large; otherwise, printing becomes slower and slower.
*/
if (COMPILED && sText.length > 8192) sText = sText.substr(sText.length - 4096);
control.value = sText;
control.scrollTop = control.scrollHeight;
}
/**
* Component.bindExternalControl(component, sControl, sBinding, sType)
*
* @param {Component} component
* @param {string} sControl
* @param {string} sBinding
* @param {string} [sType] is the external component type
*/
static bindExternalControl(component, sControl, sBinding, sType)
{
if (sControl) {
if (sType === undefined) sType = "Panel";
var target = Component.getComponentByType(sType, component.id);
if (target) {
var eBinding = target.bindings[sControl];
if (eBinding) {
component.setBinding(null, sBinding, eBinding);
}
}
}
}
/**
* Component.bindComponentControls(component, element, sAppClass)
*
* @param {Component} component
* @param {HTMLElement} element from the DOM
* @param {string} sAppClass
*/
static bindComponentControls(component, element, sAppClass)
{
var aeControls = Component.getElementsByClass(element.parentNode, sAppClass + "-control");
for (var iControl = 0; iControl < aeControls.length; iControl++) {
var aeChildNodes = aeControls[iControl].childNodes;
for (var iNode = 0; iNode < aeChildNodes.length; iNode++) {
var control = aeChildNodes[iNode];
if (control.nodeType !== 1 /* document.ELEMENT_NODE */) {
continue;
}
var sClass = control.getAttribute("class");
if (!sClass) continue;
var aClasses = sClass.split(" ");
for (var iClass = 0; iClass < aClasses.length; iClass++) {
var parms;
sClass = aClasses[iClass];
switch (sClass) {
case sAppClass + "-binding":
parms = Component.getComponentParms(control);
if (parms && parms['binding']) {
component.setBinding(parms['type'], parms['binding'], control, parms['value']);
} else if (!parms || parms['type'] != "description") {
Component.log("Component '" + component.toString() + "' missing binding" + (parms? " for " + parms['type'] : ""), "warning");
}
iClass = aClasses.length;
break;
default:
// if (DEBUG) Component.log("Component.bindComponentControls(" + component.toString() + "): unrecognized control class \"" + sClass + "\"", "warning");
break;
}
}
}
}
}
/**
* Component.getComponents(idRelated)
*
* We could store components as properties, using the component's ID, and change
* this linear lookup into a property lookup, but some components may have no ID.
*
* @param {string} [idRelated] of related component
* @return {Array} of components
*/
static getComponents(idRelated)
{
var i;
var aComponents = [];
/*
* getComponentByID(id, idRelated)
*
* If idRelated is provided, we check it for a machine prefix, and use any
* existing prefix to constrain matches to IDs with the same prefix, in order to
* avoid matching components belonging to other machines.
*/
if (idRelated) {
if ((i = idRelated.indexOf('.')) > 0)
idRelated = idRelated.substr(0, i + 1);
else
idRelated = "";
}
for (i = 0; i < Component.components.length; i++) {
var component = Component.components[i];
if (!idRelated || !component.id.indexOf(idRelated)) {
aComponents.push(component);
}
}
return aComponents;
}
/**
* Component.getComponentByID(id, idRelated)
*
* We could store components as properties, using the component's ID, and change
* this linear lookup into a property lookup, but some components may have no ID.
*
* @param {string} id of the desired component
* @param {string} [idRelated] of related component
* @return {Component|null}
*/
static getComponentByID(id, idRelated)
{
if (id !== undefined) {
var i;
/*
* If idRelated is provided, we check it for a machine prefix, and use any
* existing prefix to constrain matches to IDs with the same prefix, in order to
* avoid matching components belonging to other machines.
*/
if (idRelated && (i = idRelated.indexOf('.')) > 0) {
id = idRelated.substr(0, i + 1) + id;
}
for (i = 0; i < Component.components.length; i++) {
if (Component.components[i].id === id) {
return Component.components[i];
}
}
if (Component.components.length) {
Component.log("Component ID '" + id + "' not found", "warning");
}
}
return null;
}
/**
* Component.getComponentByType(sType, idRelated, componentPrev)
*
* @param {string} sType of the desired component
* @param {string} [idRelated] of related component
* @param {Component|null} [componentPrev] of previously returned component, if any
* @return {Component|null}
*/
static getComponentByType(sType, idRelated, componentPrev)
{
if (sType !== undefined) {
var i;
/*
* If idRelated is provided, we check it for a machine prefix, and use any
* existing prefix to constrain matches to IDs with the same prefix, in order to
* avoid matching components belonging to other machines.
*/
if (idRelated) {
if ((i = idRelated.indexOf('.')) > 0) {
idRelated = idRelated.substr(0, i + 1);
} else {
idRelated = "";
}
}
for (i = 0; i < Component.components.length; i++) {
if (componentPrev) {
if (componentPrev == Component.components[i]) componentPrev = null;
continue;
}
if (sType == Component.components[i].type && (!idRelated || !Component.components[i].id.indexOf(idRelated))) {
return Component.components[i];
}
}
Component.log("Component type '" + sType + "' not found", "warning");
}
return null;
}
/**
* Component.getComponentParms(element)
*
* @param {HTMLElement} element from the DOM
*/
static getComponentParms(element)
{
var parms = null;
var sParms = element.getAttribute("data-value");
if (sParms) {
try {
parms = eval("(" + sParms + ")"); // jshint ignore:line
/*
* We can no longer invoke removeAttribute() because some components (eg, Panel) need
* to run their initXXX() code more than once, to avoid initialization-order dependencies.
*
* if (!DEBUG) {
* element.removeAttribute("data-value");
* }
*/
} catch(e) {
Component.error(e.message + " (" + sParms + ")");
}
}
return parms;
}
/**
* Component.getElementsByClass(element, sClass, sObjClass)
*
* This is a cross-browser helper function, since not all browser's support getElementsByClassName()
*
* TODO: This should probably be moved into weblib.js at some point, along with the control binding functions above,
* to keep all the browser-related code together.
*
* @param {HTMLDocument|HTMLElement|Node} element from the DOM
* @param {string} sClass
* @param {string} [sObjClass]
* @return {Array|NodeList}
*/
static getElementsByClass(element, sClass, sObjClass)
{
if (sObjClass) sClass += '-' + sObjClass + "-object";
/*
* Use the browser's built-in getElementsByClassName() if it appears to be available
* (for example, it's not available in IE8, but it should be available in IE9 and up)
*/
if (element.getElementsByClassName) {
return element.getElementsByClassName(sClass);
}
var i, j, ae = [];
var aeAll = element.getElementsByTagName("*");
var re = new RegExp('(^| )' + sClass + '( |$)');
for (i = 0, j = aeAll.length; i < j; i++) {
if (re.test(aeAll[i].className)) {
ae.push(aeAll[i]);
}
}
if (!ae.length) {
Component.log('No elements of class "' + sClass + '" found');
}
return ae;
}
/**
* Component.getScriptCommands(sScript)
*
* This is a simple parser that breaks sScript into an array of commands, where each command
* is an array of tokens, where tokens are sequences of characters separated by any of: tab, space,
* carriage-return (CR), line-feed (LF), semicolon, single-quote, or double-quote; if a quote is
* used, all characters up to the next matching quote become part of the token, allowing any of the
* other separators to be part of the token. CR, LF and semicolon also serve to terminate a command,
* with semicolon being preferred, because it's 1) more visible, and 2) essential when the entire
* script is a multi-line string where all CR/LF were replaced by spaces (which is what Jekyll does,
* and since we can't change Jekyll, it's what our own MarkDown Front Matter parser does as well;
* see convertMD() in markout.js, where the aCommandDefs array is built).
*
* Backslash sequences like \n, \r, and \\ have already been converted to LF, CR and backslash
* characters, since the entire script string is injected into a JavaScript function call, so any
* backslash sequence that JavaScript supports is automatically converted:
*
* \0 \' \" \\ \n \r \v \t \b \f \uXXXX \xXX
* ^J ^M ^K ^I ^H ^L
*
* To support any other non-printable 8-bit character, such as ESC, you should use \xXX, where XX
* is the ASCII code in hex. For ESC, that would be \x1B.
*
* @param {string} sScript
* @return {Array}
*/
static getScriptCommands(sScript)
{
var cch = sScript.length;
var aCommands = [], aTokens = [], sToken = "", chQuote = null;
for (var i = 0; i < cch; i++) {
var ch = sScript[i];
if (ch == '"' || ch == "'") {
if (chQuote && ch != chQuote) {
sToken += ch;
continue;
}
if (!chQuote) {
chQuote = ch;
} else {
chQuote = null;
}
if (sToken) {
aTokens.push(sToken);
sToken = "";
}
continue;
}
if (!chQuote) {
if (ch == '\r' || ch == '\n') {
ch = ';';
}
if (ch == ' ' || ch == '\t' || ch == ';') {
if (sToken) {
aTokens.push(sToken);
sToken = "";
}
if (ch == ';' && aTokens.length) {
aCommands.push(aTokens);
aTokens = [];
}
continue;
}
}
sToken += ch;
}
if (sToken) {
aTokens.push(sToken);
}
if (aTokens.length) {
aCommands.push(aTokens);
}
return aCommands;
}
/**
* Component.processScript(idMachine, sScript)
*
* @param {string} idMachine
* @param {string} sScript
* @return {boolean}
*/
static processScript(idMachine, sScript)
{
var fSuccess = false;
idMachine += ".machine";
if (typeof sScript == "string" && !Component.commands[idMachine]) {
fSuccess = true;
Component.commands[idMachine] = Component.getScriptCommands(sScript);
if (!Component.processCommands(idMachine)) {
fSuccess = false;
}
}
return fSuccess;
}
/**
* Component.processCommands(idMachine)
*
* @param {string} idMachine
* @return {boolean}
*/
static processCommands(idMachine)
{
var fSuccess = true;
var aCommands = Component.commands[idMachine];
// var dbg = Component.getComponentByType("Debugger", idMachine);
while (aCommands && aCommands.length) {
var aTokens = aCommands.splice(0, 1)[0];
var sCommand = aTokens[0];
/*
* It's possible to route this output to the Debugger window with dbg.println()
* instead, but it's a bit too confusing mingling script output in a window that
* already mingles Debugger and machine output.
*/
Component.println(aTokens.join(' '), Component.TYPE.SCRIPT);
var fnCallReady = null;
if (Component.asyncCommands.indexOf(sCommand) >= 0) {
fnCallReady = function processNextCommand() {
return function() {
Component.processCommands(idMachine);
}
}();
}
var fnCommand = Component.globalCommands[sCommand];
if (fnCommand) {
if (!fnCallReady) {
fSuccess = fnCommand(aTokens[1], aTokens[2], aTokens[3]);
} else {
if (!fnCommand(fnCallReady, aTokens[1], aTokens[2], aTokens[3])) break;
}
}
else {
fSuccess = false;
var component = Component.getComponentByType(aTokens[1], idMachine);
if (component) {
fnCommand = Component.componentCommands[sCommand];
if (fnCommand) {
fSuccess = fnCommand(component, aTokens[2], aTokens[3]);
}
else {
var exports = component['exports'];
if (exports) {
fnCommand = exports[sCommand];
if (fnCommand) {
fSuccess = true;
if (!fnCallReady) {
fSuccess = fnCommand.call(component, aTokens[2], aTokens[3]);
} else {
if (!fnCommand.call(component, fnCallReady, aTokens[2], aTokens[3])) break;
}
}
}
}
}
}
if (!fSuccess) {
Component.alertUser("Script error: " + sCommand + (fnCommand? " failed" : " unrecognized"));
break;
}
}
if (aCommands && !aCommands.length) {
delete Component.commands[idMachine];
}
return fSuccess;
}
/**
* Component.scriptAlert(sMessage)
*
* @param {string} sMessage
* @return {boolean}
*/
static scriptAlert(sMessage)
{
Component.alertUser(sMessage);
return true;
}
/**
* Component.scriptSelect(component, sBinding, sValue)
*
* @param {Component} component
* @param {string} sBinding
* @param {string} sValue
* @return {boolean}
*/
static scriptSelect(component, sBinding, sValue)
{
var fSuccess = false;
var aBindings = component['bindings'];
var control = aBindings[sBinding];
if (control) {
for (var i = 0; i < control.options.length; i++) {
if (control.options[i].textContent == sValue) {
if (control.selectedIndex != i) {
control.selectedIndex = i;
}
fSuccess = true;
break;
}
}
}
return fSuccess;
}
/**
* Component.scriptSleep(fnCallback, sDelay)
*
* @param {function()} fnCallback
* @param {string} sDelay (in milliseconds)
* @return {boolean}
*/
static scriptSleep(fnCallback, sDelay)
{
setTimeout(fnCallback, +sDelay);
return false;
}
/**
* toString()
*
* @this {Component}
* @return {string}
*/
toString()
{
return (this.name? this.name : (this.id || this.type));
}
/**
* getMachineNum()
*
* @this {Component}
* @return {number} unique machine number
*/
getMachineNum()
{
var nMachine = 1;
if (this.idMachine) {
var aDigits = this.idMachine.match(/\d+/);
if (aDigits !== null)
nMachine = parseInt(aDigits[0], 10);
}
return nMachine;
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* Component's setBinding() method is intended to be overridden by subclasses.
*
* @this {Component}
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, 'print')
* @param {HTMLElement} 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(sHTMLType, sBinding, control, sValue)
{
switch (sBinding) {
case 'clear':
if (!this.bindings[sBinding]) {
this.bindings[sBinding] = control;
control.onclick = (function(component) {
return function clearControl() {
if (component.bindings['print']) {
component.bindings['print'].value = "";
}
};
}(this));
}
return true;
case 'print':
if (!this.bindings[sBinding]) {
var controlTextArea = /** @type {HTMLTextAreaElement} */ (control);
this.bindings[sBinding] = controlTextArea;
/**
* Override this.notice() with a replacement function that eliminates the Component.alertUser() call.
*
* @this {Component}
* @param {string} s
* @param {boolean} [fPrintOnly]
* @param {string} [id]
* @return {boolean}
*/
this.notice = function noticeControl(s, fPrintOnly, id) {
this.println(s, this.type);
return true;
};
/*
* This was added for Firefox (Safari will clear the <textarea> on a page reload, but Firefox does not).
*/
controlTextArea.value = "";
this.print = function(control) {
return function printControl(s) {
Component.appendControl(control, s);
};
}(controlTextArea);
this.println = function(component, control) {
return function printlnControl(s, type, id) {
if (!s) s = "";
if (type != Component.TYPE.PROGRESS || s.slice(-3) != "...") {
if (type) s = type + ": " + s;
Component.appendControl(control, s + '\n');
} else {
Component.replaceControl(control, s, s + '.');
}
if (!COMPILED && window && window.console) Component.println(s, type, id);
};
}(this, controlTextArea);
}
return true;
default:
return false;
}
}
/**
* log(s, type)
*
* For diagnostic output only.
*
* WARNING: Even though this function's body is completely wrapped in DEBUG, that won't prevent the Closure Compiler
* from including it, so all calls must still be prefixed with "if (DEBUG) ....". For this reason, the class method,
* Component.log(), is preferred, because the compiler IS smart enough to remove those calls.
*
* @this {Component}
* @param {string} [s] is the message text
* @param {string} [type] is the message type
*/
log(s, type)
{
if (!COMPILED) {
Component.log(s, type || this.id || this.type);
}
}
/**
* assert(f, s)
*
* Verifies conditions that must be true (for DEBUG builds only).
*
* WARNING: Make sure you preface all calls to this.assert() with "if (DEBUG)", because unlike Component.assert(),
* the Closure Compiler can't be sure that this instance method hasn't been overridden, so it refuses to treat it as
* dead code in non-DEBUG builds.
*
* TODO: Add a task to the build process that "asserts" there are no instances of "assertion failure" in RELEASE builds.
*
* @this {Component}
* @param {boolean|number} f is the expression asserted to be true
* @param {string} [s] is a description of the assertion to be displayed or logged on failure
*/
assert(f, s)
{
if (DEBUG) {
if (!f) {
s = "assertion failure in " + (this.id || this.type) + (s? ": " + s : "");
if (DEBUGGER && this.dbg) {
this.dbg.stopCPU();
/*
* Why do we throw an Error only to immediately catch and ignore it? Simply to give
* any IDE the opportunity to inspect the application's state. Even when the IDE has
* control, you should still be able to invoke Debugger commands from the IDE's REPL,
* using the '$' global function that the Debugger constructor defines; eg:
*
* $('r')
* $('dw 0:0')
* $('h')
* ...
*
* If you have no desire to stop on assertions, consider this a no-op. However, another
* potential benefit of creating an Error object is that, for browsers like Chrome, we get
* a stack trace, too.
*/
try {
throw new Error(s);
} catch(e) {
this.println(e.stack || e.message);
}
return;
}
this.log(s);
throw new Error(s);
}
}
}
/**
* print(s)
*
* Components using this.print() should wait until after their constructor has run to display any messages, because
* if a Control Panel has been loaded, its override will not take effect until its own constructor has run.
*
* @this {Component}
* @param {string} s
*/
print(s)
{
Component.print(s);
}
/**
* println(s, type, id)
*
* Components using this.println() should wait until after their constructor has run to display any messages, because
* if a Control Panel has been loaded, its override will not take effect until its own constructor has run.
*
* @this {Component}
* @param {string} [s] is the message text
* @param {string} [type] is the message type
* @param {string} [id] is the caller's ID, if any
*/
println(s, type, id)
{
Component.println(s, type, id || this.id);
}
/**
* status(s)
*
* status() is like println() but it also includes information about the component (ie, the component type),
* which is why there is no corresponding Component.status() function.
*
* @param {string} s is the message text
*/
status(s)
{
this.println(this.type + ": " + s);
}
/**
* notice(s, fPrintOnly, id)
*
* notice() is like println() but implies a need for user notification, so we alert() as well; however, if this.println()
* is overridden, this.notice will be replaced with a similar override, on the assumption that the override is taking care
* of alerting the user.
*
* @this {Component}
* @param {string} s is the message text
* @param {boolean} [fPrintOnly]
* @param {string} [id] is the caller's ID, if any
* @return {boolean}
*/
notice(s, fPrintOnly, id)
{
if (!fPrintOnly) {
/*
* See if the associated computer, if any, is "unloading"....
*/
var computer = Component.getComponentByType("Computer", this.id);
if (computer && computer.flags.unloading) {
console.log("ignoring notice during unload: " + s);
return false;
}
}
Component.notice(s, fPrintOnly, id || this.type);
return true;
}
/**
* setError(s)
*
* Set a fatal error condition
*
* @this {Component}
* @param {string} s describes a fatal error condition
*/
setError(s)
{
this.flags.error = true;
this.notice(s); // TODO: Any cases where we should still prefix this string with "Fatal error: "?
}
/**
* clearError()
*
* Clear any fatal error condition
*
* @this {Component}
*/
clearError() {
this.flags.error = false;
}
/**
* isError()
*
* Report any fatal error condition
*
* @this {Component}
* @return {boolean} true if a fatal error condition exists, false if not
*/
isError()
{
if (this.flags.error) {
this.println(this.toString() + " error");
return true;
}
return false;
}
/**
* isReady(fnReady)
*
* Return the "ready" state of the component; if the component is not ready, it will queue the optional
* notification function, otherwise it will immediately call the notification function, if any, without queuing it.
*
* NOTE: Since only the Computer component actually cares about the "readiness" of other components, the so-called
* "queue" of notification functions supports exactly one function. This keeps things nice and simple.
*
* @this {Component}
* @param {function()} [fnReady]
* @return {boolean} true if the component is in a "ready" state, false if not
*/
isReady(fnReady)
{
if (fnReady) {
if (this.flags.ready) {
fnReady();
} else {
if (MAXDEBUG) this.log("NOT ready");
this.fnReady = fnReady;
}
}
return this.flags.ready;
}
/**
* setReady(fReady)
*
* Set the "ready" state of the component to true, and call any queued notification functions.
*
* @this {Component}
* @param {boolean} [fReady] is assumed to indicate "ready" unless EXPLICITLY set to false
*/
setReady(fReady)
{
if (!this.flags.error) {
this.flags.ready = (fReady !== false);
if (this.flags.ready) {
if (MAXDEBUG /* || this.name */) this.log("ready");
var fnReady = this.fnReady;
this.fnReady = null;
if (fnReady) fnReady();
}
}
}
/**
* isBusy(fCancel)
*
* Return the "busy" state of the component
*
* @this {Component}
* @param {boolean} [fCancel] is set to true to cancel a "busy" state
* @return {boolean} true if "busy", false if not
*/
isBusy(fCancel)
{
if (this.flags.busy) {
if (fCancel) {
this.flags.busyCancel = true;
} else if (fCancel === undefined) {
this.println(this.toString() + " busy");
}
}
return this.flags.busy;
}
/**
* setBusy(fBusy)
*
* Update the current busy state; if a busyCancel request is pending, it will be honored now.
*
* @this {Component}
* @param {boolean} fBusy
* @return {boolean}
*/
setBusy(fBusy)
{
if (this.flags.busyCancel) {
this.flags.busy = false;
this.flags.busyCancel = false;
return false;
}
if (this.flags.error) {
this.println(this.toString() + " error");
return false;
}
this.flags.busy = fBusy;
return this.flags.busy;
}
/**
* powerUp(fSave)
*
* @this {Component}
* @param {Object|null} data
* @param {boolean} [fRepower] is true if this is "repower" notification
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
this.flags.powered = true;
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {Component}
* @param {boolean} fSave
* @param {boolean} [fShutdown]
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
if (fShutdown) this.flags.powered = false;
return true;
}
/**
* messageEnabled(bitsMessage)
*
* If bitsMessage is not specified, the component's MESSAGE category is used.
*
* @this {Component}
* @param {number} [bitsMessage] is zero or more MESSAGE_* category flag(s)
* @return {boolean} true if all specified message enabled, false if not
*/
messageEnabled(bitsMessage)
{
if (DEBUGGER && this.dbg) {
if (this === this.dbg) {
bitsMessage |= 0;
} else {
bitsMessage = bitsMessage || this.bitsMessage;
}
var bitsEnabled = this.dbg.bitsMessage & bitsMessage;
return (!!bitsMessage && bitsEnabled === bitsMessage || !!(bitsEnabled & this.dbg.bitsWarning));
}
return false;
}
/**
* printMessage(sMessage, bitsMessage, fAddress)
*
* If bitsMessage is not specified, the component's MESSAGE category is used.
* If bitsMessage is true, the message is displayed regardless.
*
* @this {Component}
* @param {string} sMessage is any caller-defined message string
* @param {number|boolean} [bitsMessage] is zero or more MESSAGE_* category flag(s)
* @param {boolean} [fAddress] is true to display the current address
*/
printMessage(sMessage, bitsMessage, fAddress)
{
if (DEBUGGER && this.dbg) {
if (bitsMessage === true || this.messageEnabled(bitsMessage | 0)) {
this.dbg.message(sMessage, fAddress);
}
}
}
/**
* printMessageIO(port, bOut, addrFrom, name, bIn, bitsMessage)
*
* If bitsMessage is not specified, the component's MESSAGE category is used.
* If bitsMessage is true, the message is displayed as long as MESSAGE.PORT is enabled.
*
* @this {Component}
* @param {number} port
* @param {number|null} bOut if an output operation
* @param {number|null} [addrFrom]
* @param {string|null} [name] of the port, if any
* @param {number|null} [bIn] is the input value, if known, on an input operation
* @param {number|boolean} [bitsMessage] is zero or more MESSAGE_* category flag(s)
*/
printMessageIO(port, bOut, addrFrom, name, bIn, bitsMessage)
{
if (DEBUGGER && this.dbg) {
if (bitsMessage === true) {
bitsMessage = 0;
} else if (bitsMessage == null) {
bitsMessage = this.bitsMessage;
}
this.dbg.messageIO(this, port, bOut, addrFrom, name, bIn, bitsMessage);
}
}
}
/*
* These are the standard TYPE values you can pass as an optional argument to println(); in reality,
* you can pass anything you want, because they are simply prepended to the message, although PROGRESS
* messages may also be merged with earlier similar messages to keep the output buffer under control.
*/
Component.TYPE = {
ERROR: "error",
NOTICE: "notice",
PROGRESS: "progress",
SCRIPT: "script",
WARNING: "warning"
};
/*
* Every component created on the current page is recorded in this array (see Component.add()),
* enabling any component to locate another component by ID (see Component.getComponentByID())
* or by type (see Component.getComponentByType()).
*
* Every machine on the page are now recorded as well, by their machine ID. We then record the
* various resources used by that machine.
*
* Includes a fallback for non-browser-based environments (ie, Node). TODO: This will need to be
* tailored to Node, probably using the global object instead of the window object, if we ever want
* to support multi-machine configs in that environment.
*/
if (window) {
if (!window['PCjs']) window['PCjs'] = {};
if (!window['PCjs']['Machines']) window['PCjs']['Machines'] = {};
if (!window['PCjs']['Components']) window['PCjs']['Components'] = [];
if (!window['PCjs']['Commands']) window['PCjs']['Commands'] = {};
}
Component.machines = window? window['PCjs']['Machines'] : {};
Component.components = window? window['PCjs']['Components'] : [];
Component.commands = window? window['PCjs']['Commands'] : {};
Component.asyncCommands = [
'hold', 'sleep', 'wait'
];
Component.globalCommands = {
'alert': Component.scriptAlert,
'sleep': Component.scriptSleep
};
Component.componentCommands = {
'select': Component.scriptSelect
};
Component.printBuffer = "";
/*
* The following polyfills provide ES5 functionality that's missing in older browsers (eg, IE8),
* allowing PCjs apps to run without slamming into exceptions; however, due to the lack of HTML5 canvas
* support in those browsers, all you're likely to see are "soft" errors (eg, "Missing <canvas> support").
*
* Perhaps we can implement a text-only faux video display for a fun retro-browser experience someday.
*
* TODO: Come up with a better place to put these polyfills. We will likely have more if we decide to
* make the leap from ES5 to ES6 features.
*/
/*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
*/
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(obj, start) {
for (var i = (start || 0), j = this.length; i < j; i++) {
if (this[i] === obj) { return i; }
}
return -1;
}
}
/*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
*/
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
/*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
*/
if (!Function.prototype.bind) {
Function.prototype.bind = function(obj) {
if (typeof this != "function") {
// Closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind: non-callable object");
}
var args = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
var fnNOP = /** @constructor */ (function() {});
var fnBound = function() {
return fToBind.apply(this instanceof fnNOP && obj? this : obj, args.concat(Array.prototype.slice.call(arguments)));
};
fnNOP.prototype = this.prototype;
fnBound.prototype = new fnNOP();
return fnBound;
};
}
/**
* @copyright http://pcjs.org/modules/pdp10/lib/defines.js (C) Jeff Parsons 2012-2017
*/
/**
* @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
/*
* Set this to true to enable behavior compatible with SIMH.
*/
var SIMH = false;
/*
* 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)
*/
MODEL_KA10: 1001,
/*
* ADDR_INVALID 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_MASK: Math.pow(2, 18) - 1,
ADDR_LIMIT: Math.pow(2, 18),
/*
* 18-bit and 36-bit largest positive (and smallest negative) values; however, since we store all
* values as unsigned quantities, these are the unsigned equivalents.
*/
WORD_INVALID: -1,
HINT_MASK: Math.pow(2, 17) - 1, // 131,071 (377777) signed half-word (half-int) mask
HINT_LIMIT: Math.pow(2, 17), // 131,072 (400000) signed half-word (half-int) limit
HALF_MASK: Math.pow(2, 18) - 1, // 262,143 (000000 777777): unsigned half-word mask
HALF_SHIFT: Math.pow(2, 18), // 262,144 (000001 000000): unsigned half-word shift
INT_MASK: Math.pow(2, 35) - 1, // 34,359,738,367 (377777 777777): signed word (magnitude) mask
INT_LIMIT: Math.pow(2, 35), // 34,359,738,368 (400000 000000): signed word (magnitude) limit
WORD_MASK: Math.pow(2, 36) - 1, // 68,719,476,735 (777777 777777): unsigned word mask
WORD_LIMIT: Math.pow(2, 36), // 68,719,476,736 (1 000000 000000): unsigned word limit
TWO_POW32: Math.pow(2, 32),
TWO_POW34: Math.pow(2, 34),
TWO_POW36: Math.pow(2, 36), // the two's complement of a 36-bit value is (value? TWO_POW36 - value : 0)
/*
* PDP-10 opcodes are 36-bit values, most of which use the following layout:
*
* 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
* O O O O O O O M M A A A A I X X X X Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
*
* or using modern bit-numbering:
*
* 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
* 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
* O O O O O O O M M A A A A I X X X X Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
*
* where OOOOOOOMM represents the operation, and MM (if used) represents the mode:
*
* Mode Suffix Source Destination
* ---- ------ ----- -----------
* 0: Basic None E AC
* 1: Immediate I 0,E AC
* 2: Memory M AC E
* 3: Self/Both S or B E E (and AC if A is non-zero)
*
* Input-output instructions look like:
*
* 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
* 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
* 1 1 1 D D D D D D D O O O I X X X X Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
*
* Bits 0-22 (I,X,Y) contain what we call a "reference address" (R), which is used to calculate the
* "effective address" (E). To determine E from R, we must extract I, X, and Y from R, set E to Y,
* then add [X] to E if X is non-zero. If I is zero, then we're done; otherwise, we must set R to [E]
* and repeat the process.
*/
OPCODE: {
OPMASK: 0o77700, // operation mask
OPMODE: 0o77400, // operation with mode
OPCOMP: 0o77000, // operation with compare
OPTEST: 0o71100, // operation with test
OPIO: 0o70034, // input-output operation
OPUUO: 0o70000, // unimplemented user operation (UUO) mask
OP_SCALE: Math.pow(2, 21), // operation scale
IO_SCALE: Math.pow(2, 26), // input-output device code scale
IO_MASK: 0o177, // input-output device code mask (after descale)
A_SCALE: Math.pow(2, 23), // used to shift down the high 13 bits, with A starting at bit 0
P_SCALE: Math.pow(2, 30), // P scale
P_MASK: 0o77, // P mask (after descale)
S_SHIFT: 24, // S shift
S_MASK: 0o77, // S mask (after shift)
A_SHIFT: 23, // A shift
A_MASK: 0o17, // A mask (after shift)
A_FIELD: 0o740000000, // A field mask
I_FIELD: 0o20000000, // indirect bit mask
X_SHIFT: 18, // X shift
X_MASK: 0o17, // X mask (after shift)
X_FIELD: 0o17000000, // X field mask
Y_SHIFT: 0, // Y shift
Y_MASK: 0o777777, // Y mask (after shift)
Y_FIELD: 0o777777, // Y field mask
R_MASK: 0o37777777, // used to isolate the low 23 bits (I,X,Y)
PTR_MASK: 0o77777777, // used to isolate the low 24 bits (?,I,X,Y) of a byte pointer
HALT: 0o5304 // operation code for HALT
},
/*
* 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
},
/*
* Flags returned by getPS() for various program control operations.
*
* NOTE: I see SIMH setting PS bits like 0o000200 and 0o000400, which are not documented for the KA10.
* The SIMH docs only refer to the KS10 ("KS10 CPU with 1MW of memory"), so I'm guessing it doesn't have
* a KA10 emulation option. The `pdp10` SIMH binary does have some SET CPU options, but unlike the `pdp11`
* binary, the only options you can set relate to the operating system to be run -- which seems very hacky.
*/
PSFLAG: {
AROV: 0o400000, // Arithmetic Overflow
CRY0: 0o200000, // Carry 0
CRY1: 0o100000, // Carry 1
FOV: 0o040000, // Floating-Point Overflow
BIS: 0o020000, // Byte Interrupt
USERF: 0o010000, // User Mode Flag
EXIOT: 0o004000, // User Privileged I/O Flag
FXU: 0o000100, // Floating-Point Underflow
DCK: 0o000040, // Divide Check (aka No Divide)
/*
* Only the low 18 bits (above) are returned by getPS(); the following (bits 18 to 31)
* are defined for internal use only.
*/
PDOV: 0o1000000, // Pushdown Overflow
SET_MASK: 0o0760140 // flags that are always settable/clearable
},
/*
* Readable CPU (or APR for "Arithmetic Processor") flags provided by the "CONI APR," instruction; see opCONI().
*/
RFLAG: {
PIA: 0o000007, // Priority Interrupt Assignment
AROV: 0o000010, // Arithmetic Overflow
AROV_IE: 0o000020, // Arithmetic Overflow Interrupt Enabled
TRAP_OFF: 0o000040, // Trap Offset
FOV: 0o000100, // Floating-Point Overflow
FOV_IE: 0o000200, // Floating-Point Overflow Interrupt Enabled
CLK: 0o001000, // Clock Flag
CLK_IE: 0o002000, // Clock Interrupt Enabled
NXM: 0o010000, // Non-Existent Memory
PRM: 0o020000, // Memory Protection
ADB: 0o040000, // Address Break
UIO: 0o100000, // User In-Out
PDOV: 0o200000 // Pushdown Overflow (TODO: Verify this is correct; the May 1968 doc may have a typo)
},
/*
* Writable CPU (or APR for "Arithmetic Processor") flags provided by the "CONO APR," instruction; see opCONO().
*
* A set bit performs the function shown below, a clear bit does nothing.
*/
WFLAG: {
PIA: 0o000007, // Priority Interrupt Assignment
AROV_CL: 0o000010, // Clear Overflow
AROV_IE: 0o000020, // Enable Overflow Interrupt
AROV_ID: 0o000040, // Disable Overflow Interrupt
FOV_CL: 0o000100, // Clear Floating-Point Overflow
FOV_IE: 0o000200, // Enable Floating-Point Overflow Interrupt
FOV_ID: 0o000400, // Disable Floating-Point Overflow Interrupt
CLK_CL: 0o001000, // Clear Clock Flag
CLK_IE: 0o002000, // Enable Clock Interrupt
CLK_ID: 0o004000, // Disable Clock Interrupt
NXM_CL: 0o010000, // Clear Non-Existent Memory
PRM_CL: 0o020000, // Clear Memory Protection
ADB_CL: 0o040000, // Clear Address Break
UIO_CL: 0o200000, // Clear All In-Out Devices
PDOV_CL: 0o400000 // Clear Pushdown Overflow
},
/*
* 7-bit device codes used by Input-Output instructions; see opIO().
*/
DEVICES: {
APR: 0o000, // Arithmetic Processor
PI: 0o001 // Priority Interrupt
}
};
/*
* 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;
PDP10.SIMH = SIMH;
/**
* @copyright http://pcjs.org/modules/pdp10/lib/messages.js (C) Jeff Parsons 2012-2017
*/
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
};
/**
* @copyright http://pcjs.org/modules/pdp10/lib/panel.js (C) Jeff Parsons 2012-2017
*/
/**
* 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 {HTMLElement} 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 'PC':
this.bindings[sBinding] = control;
this.cLiveRegs++;
return true;
default:
/*
* Square ("led") or round ("rled") LEDs are defined in machine XML files like so:
*
* <control type="rled" binding="A3" value="1" width="100%" container="center"/>
*
* 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:
*
* <control type="switch" binding="S3" value="1" width="100%" container="center"/>
*
* 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.WORD_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);
/**
* @copyright http://pcjs.org/modules/pdp10/lib/bus.js (C) Jeff Parsons 2012-2017
*/
/*
* 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.<BlockInfoPDP10>
* }}
*/
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.nDisableFaults = 0;
this.fFault = false;
/*
* 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++];
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 (36-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 (36-bit) value to write
*/
setWord(addr, w)
{
var off = addr & this.nBlockLimit;
var iBlock = (addr & this.nBusMask) >>> this.nBlockShift;
this.aBusBlocks[iBlock].writeWord(w, off, 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 (36-bit) value at that address
*/
getWordDirect(addr)
{
var w;
var off = addr & this.nBlockLimit;
var block = this.getBlockDirect(addr);
this.nDisableFaults++;
w = block.readWordDirect(off, addr);
this.nDisableFaults--;
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 (36-bit) value to write
*/
setWordDirect(addr, w)
{
var off = addr & this.nBlockLimit;
var block = this.getBlockDirect(addr);
this.nDisableFaults++;
block.writeWordDirect(w, off, addr);
this.nDisableFaults--;
}
/**
* 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 bitwise 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;
}
/**
* fault(addr, err, access)
*
* Bus interface for signaling alignment errors, invalid memory, etc.
*
* @this {BusPDP10}
* @param {number} addr
* @param {number} [err]
* @param {number} [access] (for diagnostic purposes only)
*/
fault(addr, err, access)
{
this.fFault = true;
if (!this.nDisableFaults) {
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(MessagesPDP10.FAULT)) {
this.dbg.printMessage("memory fault on " + this.dbg.toStrBase(addr), true, true);
}
this.cpu.haltCPU();
}
}
/**
* checkFault()
*
* This also serves as a clearFault() function.
*
* @this {BusPDP10}
* @return {boolean}
*/
checkFault()
{
var f = this.fFault;
this.fFault = false;
return f;
}
/**
* 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
};
/**
* @copyright http://pcjs.org/modules/pdp10/lib/device.js (C) Jeff Parsons 2012-2017
*/
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);
/**
* @copyright http://pcjs.org/modules/pdp10/lib/memory.js (C) Jeff Parsons 2012-2017
*/
/**
* @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.readWord = this.readWordDirect = this.readNone;
this.writeWord = this.writeWordDirect = this.writeNone;
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.<number>|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.<number>|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] (default is zero)
*/
zero(off, len, pattern = 0)
{
/*
* NOTE: If len is larger than the block, that's OK, because we also bounds-check the index.
*/
off = off || 0;
if (len === undefined) len = this.size;
/*
* Although it's expected that most callers will supply unsigned 36-bit values, we're nice about
* converting any signed values to their unsigned (two's complement) counterpart, provided they are
* within the acceptable range. Any values outside that range will be dealt with afterward.
*/
if (pattern < 0 && pattern >= -PDP10.INT_LIMIT) {
pattern += PDP10.WORD_LIMIT;
}
pattern = Math.trunc(Math.abs(pattern)) % PDP10.WORD_LIMIT;
for (var i = off; len-- && i < this.size; i++) this.writeWordDirect(pattern, off, this.addr + off);
}
/**
* setAccess(afn, fDirect)
*
* The afn parameter should be a 2-entry function table containing word read and write 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.
*
* 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.<function()>} [afn] function table
* @param {boolean} [fDirect] (true to update direct access functions as well; default is true)
*/
setAccess(afn, fDirect)
{
if (!afn) {
afn = MemoryPDP10.afnNone;
}
this.setReadAccess(afn, fDirect);
this.setWriteAccess(afn, fDirect);
}
/**
* setReadAccess(afn, fDirect)
*
* @this {MemoryPDP10}
* @param {Array.<function()>} afn
* @param {boolean} [fDirect]
*/
setReadAccess(afn, fDirect)
{
if (!fDirect || !this.cReadBreakpoints) {
this.readWord = afn[0] || this.readNone;
}
if (fDirect || fDirect === undefined) {
this.readWordDirect = afn[0] || this.readNone;
}
}
/**
* setWriteAccess(afn, fDirect)
*
* @this {MemoryPDP10}
* @param {Array.<function()>} afn
* @param {boolean} [fDirect]
*/
setWriteAccess(afn, fDirect)
{
if (!fDirect || !this.cWriteBreakpoints) {
this.writeWord = !this.fReadOnly && afn[1] || this.writeNone;
}
if (fDirect || fDirect === undefined) {
this.writeWordDirect = afn[1] || this.writeNone;
}
}
/**
* resetReadAccess()
*
* @this {MemoryPDP10}
*/
resetReadAccess()
{
this.readWord = this.readWordDirect;
}
/**
* resetWriteAccess()
*
* @this {MemoryPDP10}
*/
resetWriteAccess()
{
this.writeWord = this.fReadOnly? this.writeNone : 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");
}
}
else {
if (--this.cWriteBreakpoints === 0) {
this.resetWriteAccess();
if (DEBUG) this.printAddr("all write breakpoints removed from memory block");
}
}
}
/**
* 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, addr)
*
* @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);
}
this.bus.fault(addr);
return PDP10.WORD_INVALID;
}
/**
* 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);
}
this.bus.fault(addr);
}
/**
* readWordMemory(off, addr)
*
* @this {MemoryPDP10}
* @param {number} off
* @param {number} addr
* @return {number}
*/
readWordMemory(off, addr)
{
var w = this.aw[off];
return w;
}
/**
* writeWordMemory(w, off, addr)
*
* @this {MemoryPDP10}
* @param {number} w
* @param {number} off
* @param {number} addr
*/
writeWordMemory(w, off, addr)
{
if (this.aw[off] != w) {
this.aw[off] = w;
this.fDirty = true;
}
}
/**
* 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);
}
/**
* 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 handlers are undefined.
*
MemoryPDP10.afnNone = [
MemoryPDP10.prototype.readNone,
MemoryPDP10.prototype.writeNone
];
*/
MemoryPDP10.afnNone = [];
MemoryPDP10.afnMemory = [
MemoryPDP10.prototype.readWordMemory,
MemoryPDP10.prototype.writeWordMemory
];
MemoryPDP10.afnChecked = [
MemoryPDP10.prototype.readWordChecked,
MemoryPDP10.prototype.writeWordChecked
];
/**
* @copyright http://pcjs.org/modules/pdp10/lib/cpu.js (C) Jeff Parsons 2012-2017
*/
/**
* @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 {
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.
*/
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];
if (timer[0] < 0) continue;
if (nCycles > timer[0]) {
nCycles = timer[0];
}
}
return nCycles;
}
/**
* saveTimers()
*
* @this {CPUPDP10}
* @return {Array.<number>}
*/
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.<number>} aTimerCycles
*/
restoreTimers(aTimerCycles)
{
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];
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"];
/**
* @copyright http://pcjs.org/modules/pdp10/lib/cpustate.js (C) Jeff Parsons 2012-2017
*/
/*
* 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.
*
* Speeds are highly instruction-specific and are not broken down into cycles; DEC documents them
* as a number of microseconds, with two decimal places of accuracy. The simplest instructions
* execute in 1-3us, a number of others require 5-6us, and the most time-consuming take anywhere
* from 10us (MUL) to 17us (DIV). Of course, instructions that perform multiple indirect memory
* accesses take even longer.
*
* I think we'll just say that the original PDP-10 was roughly a 1Mhz machine, and pretend that all
* instructions completed in 1 or more multiples of a microsecond. I'm not sure that trying to be
* accurate to the nearest 1/100 of a microsecond would have much observable benefit.
*
* @param {Object} parmsCPU
*/
constructor(parmsCPU)
{
var nCyclesDefault = 0;
var model = +parmsCPU['model'] || PDP10.MODEL_KA10;
switch(model) {
case PDP10.MODEL_KA10:
default:
nCyclesDefault = 1000000;
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;
this.opDecode = PDP10.opKA10.bind(this);
this.opUndefined = PDP10.opUndefined.bind(this);
/** @type {IRQ|null} */
this.irqNext = null; // the head of the active IRQ list, in priority order
/** @type {Array.<IRQ>} */
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()
{
/*
* regEA is the last effective address, while regLA is the last fetch from an effective address
* calculation. regRA is the last reference address used to calculate the last effective address.
*/
this.regEA = this.regRA = 0;
this.regLA = this.regOP = 0;
this.regPC = this.lastPC = this.addrReset;
this.regXC = -1; // if >= 0 this supersedes regPC (refers to an opcode from XCT)
this.regBP = -1; // active byte pointer (-1 if none)
this.regPS = 0; // assorted processor flags (see PSFLAG bit definitions)
this.regEX = 0; // internal "extension" register used for 72-bit MUL and DIV calculations
this.regRes = [0, 0]; // four internal "double-length" registers used for 72-bit DIV calculations
this.regPow = [0, 0];
this.regDiv = [0, 0];
this.regRem = [0, 0];
/*
* 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.regEA,
this.regRA,
this.regLA,
this.regOP,
this.regPC,
this.regXC,
this.regBP,
this.regPS,
this.opFlags,
this.lastPC,
this.addrLast,
this.addrReset
]);
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.regEA,
this.regRA,
this.regLA,
this.regOP,
this.regPC,
this.regXC,
this.regBP,
this.regPS,
this.opFlags,
this.lastPC,
this.addrLast,
this.addrReset
] = 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;
}
/**
* getPS()
*
* Gets the processor state flags in the format required by various program control operations (eg, JSP).
*
* @this {CPUStatePDP10}
* @return {number}
*/
getPS()
{
return (this.regPS & PDP10.HALF_MASK);
}
/**
* setPS(w)
*
* Sets the processor state flags in the format required by various program control operations (eg, JRST).
*
* @this {CPUStatePDP10}
* @param {number} w
*/
setPS(w)
{
this.regPS = (this.regPS & ~PDP10.PSFLAG.SET_MASK) | (w & PDP10.PSFLAG.SET_MASK);
this.regPS |= (w & PDP10.PSFLAG.USERF);
if (!(w & PDP10.PSFLAG.EXIOT)) {
this.regPS &= ~PDP10.PSFLAG.EXIOT;
} else {
if (!(this.regPS & PDP10.PSFLAG.USERF)) this.regPS |= PDP10.PSFLAG.EXIOT;
}
}
/**
* setUserMode()
*
* Sets the processor's USER_MODE flag.
*
* @this {CPUStatePDP10}
*/
setUserMode()
{
this.regPS |= PDP10.PSFLAG.USERF;
}
/**
* readFlags()
*
* Used to implement the ""CONI APR," instruction; see opCONI().
*
* @this {CPUStatePDP10}
* @return {number}
*/
readFlags()
{
var flags = 0;
if (this.regPS & PDP10.PSFLAG.AROV) flags |= PDP10.RFLAG.AROV;
if (this.regPS & PDP10.PSFLAG.PDOV) flags |= PDP10.RFLAG.PDOV;
return flags;
}
/**
* writeFlags(w)
*
* Used to implement the ""CONO APR," instruction; see opCONO().
*
* @this {CPUStatePDP10}
* @param {number} w
*/
writeFlags(w)
{
if (w & PDP10.WFLAG.AROV_CL) this.regPS &= ~PDP10.PSFLAG.AROV;
if (w & PDP10.WFLAG.PDOV_CL) this.regPS &= ~PDP10.PSFLAG.PDOV;
}
/**
* getOpcode()
*
* Normally, this fetches the next opcode in regOP, decodes the low 23 bits (I,X,Y), records
* the effective address (E) in regEA, updates regPC, and returns the high 13 bits of the opcode
* for further decoding.
*
* However, if a reference address (R) in regRA still needs to be decoded (due to indirection),
* we take care of that first.
*
* @this {CPUStatePDP10}
* @return {number} (-1 if the reference address in regRA has not yet been fully decoded)
*/
getOpcode()
{
if ((this.regRA & PDP10.OPCODE.I_FIELD)) {
this.regRA = this.regLA = this.readWord(this.regEA);
} else if (this.regXC >= 0) {
this.regRA = this.regOP = this.readWord(this.regXC);
this.regXC = -1;
} else {
this.regRA = this.regOP = this.readWord(this.lastPC = this.regPC);
this.regPC = (this.regPC + 1) % PDP10.ADDR_LIMIT;
}
/*
* Technically, we don't REALLY need to mask regRA with R_MASK, because all regRA accesses
* ignore any higher bits, but let's keep things tidy.
*/
this.regRA &= PDP10.OPCODE.R_MASK;
/*
* Bits 0-22 (I,X,Y) contain what we call a "reference address" (R), which is used to calculate an
* 18-bit "effective address" (E). To determine E from R, we must extract I, X, and Y from R, set E
* to Y, then add [X] to E if X is non-zero. If I is zero, then we're done; otherwise, we must set R
* to [E] and repeat the process.
*
* However, we don't actually repeat the process immediately; we need to treat each indirection as a
* separate decoding step, to ensure that the emulator can "breathe" periodically. So instead, we
* return -1, indicating that the opcode is not fully decoded, and then on the next call, instead of
* fetching another opcode, we fetch [E], update R, and decode R again.
*/
this.regEA = this.regRA & PDP10.OPCODE.Y_MASK;
var x = (this.regRA >> PDP10.OPCODE.X_SHIFT) & PDP10.OPCODE.X_MASK;
if (x) this.regEA = (this.regEA + (this.regLA = this.readWord(x))) & PDP10.ADDR_MASK;
return (this.regRA & PDP10.OPCODE.I_FIELD)? -1 : ((this.regOP / PDP10.OPCODE.A_SCALE)|0);
}
/**
* 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;
}
/**
* getXC()
*
* NOTE: This function is nothing more than a convenience, and we fully expect it to be inlined at runtime.
*
* @this {CPUStatePDP10}
* @return {number}
*/
getXC()
{
return this.regXC >= 0? this.regXC : ((this.regRA & PDP10.OPCODE.I_FIELD)? this.lastPC : this.regPC);
}
/**
* getLastAddr()
*
* @this {CPUStatePDP10}
* @return {number}
*/
getLastAddr()
{
return this.addrLast;
}
/**
* getLastPC()
*
* @this {CPUStatePDP10}
* @return {number}
*/
getLastPC()
{
return this.lastPC;
}
/**
* setPC(addr)
*
* Updates the PC register with the new address after masking it with ADDR_LIMIT (in case the
* new address was the result of an unchecked calculation).
*
* @this {CPUStatePDP10}
* @param {number} addr
*/
setPC(addr)
{
this.regRA = 0;
this.regXC = -1;
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.<number>}
*/
saveIRQs()
{
var aIRQVectors = [];
var irq = this.irqNext;
while (irq) {
aIRQVectors.push(irq.vector);
irq = irq.next;
}
return aIRQVectors;
}
/**
* restoreIRQs(aIRQVectors)
*
* @this {CPUStatePDP10}
* @param {Array.<number>} aIRQVectors
*/
restoreIRQs(aIRQVectors)
{
for (var i = aIRQVectors.length - 1; i >= 0; i--) {
var irq = this.findIRQ(aIRQVectors[i]);
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
* @return {number} (we return the data back to the caller to permit nested writes)
*/
writeWordToPhysical(addr, data)
{
this.bus.setWord(this.addrLast = addr, data);
return data;
}
/**
* haltCPU()
*
* This is a temporary helper function for the Bus component, to force the CPU to stop executing the
* current instruction.
*
* @this {CPUStatePDP10}
*/
haltCPU()
{
this.stopCPU();
throw -1;
}
/**
* 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.getXC(), 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.getXC(), 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 op = this.getOpcode();
if (op >= 0) {
this.opDecode(op);
}
/*
* TODO: This is a temporary cycle charge, required for CPU operational bookkeeping until we add
* correct cycle counts for all instructions.
*/
this.nStepCycles--;
} 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);
/**
* @copyright http://pcjs.org/modules/pdp10/lib/cpuops.js (C) Jeff Parsons 2012-2017
*/
/*
From the "PDP-10 System Reference Manual", May 1968, p. 1-4:
1.1 NUMBER SYSTEM
The program can interpret a data word as a 36-digit, unsigned binary number, or the left and right
halves of a word can be taken as separate 18-bit numbers. The PDP-10 repertoire includes instructions
that effectively add or subtract one from both halves of a word, so the right half can be used for
address modification when the word is addressed as an index register, while the left half is used to
keep a control count.
The standard arithmetic instructions in the PDP-10 use twos complement, fixed point conventions to do
binary arithmetic. In a word used as a number, bit 0 (the leftmost bit) represents the sign, 0 for positive,
1 for negative. In a positive number the remaining 35 bits are the magnitude in ordinary binary notation.
The negative of a number is obtained by taking its twos complement. If x is an n-digit binary number, its
twos complement is 2^n - x, and its ones complement is (2^n - 1) - x, or equivalently (2^n - x) - 1.
Subtracting a number from 2^n - 1 (ie, from all 1s) is equivalent to performing the logical complement,
ie changing all 0s to 1s and all 1s to 0s. Therefore, to form the twos complement one takes the logical
complement (usually referred to merely as the complement) of the entire word including the sign, and adds
1 to the result. In a negative number the sign bit is 1, and the remaining bits are the twos complement
of the magnitude.
Zero is represented by a word containing all 0s. Complementing this number produces all 1s, and adding
1 to that produces all 0s again. Hence there is only one zero representation and its sign is positive.
Since the numbers are symmetrical in magnitude about a single zero representation, all even numbers both
positive and negative end in 0, all odd numbers in 1 (a number all 1s represents -1). But since there are
the same number of positive and negative numbers and zero is positive, there is one more negative number
than there are nonzero positive numbers. This is the most negative number and it cannot be produced by
negating any positive number (its octal representation is 400000 000000 and its magnitude is one greater
than the largest positive number).
If ones complements were used for negatives one could read a negative number by attaching significance
to the as instead of the 1s. In twos complement notation each negative number is one greater than the
complement of the positive number of the same magnitude, so one can read a negative number by attaching
significance to the rightmost 1 and attaching significance to the 0s at the left of it (the negative number
of largest magnitude has a 1 in only the sign position). In a negative integer, 1s may be discarded at the
left, just as leading 0s may be dropped in a positive integer. In a negative fraction, 0s may be discarded
at the right. So long as only 0s are discarded, the number remains in twos complement form because it
still has a 1 that possesses significance; but if a portion including the rightmost 1 is discarded, the
remaining part of the fraction is now a ones complement.
The computer does not keep track of a binary point - the programmer must adopt a point convention and shift
the magnitude of the result to conform to the convention used. Two common conventions are to regard a number
as an integer (binary point at the right) or as a proper fraction (binary point at the left); in these two
cases the range of numbers represented by a single word is -2^35 to 2^35 - 1, or -1 to 1 - 2^35. Since
multiplication and division make use of double length numbers, there are special instructions for performing
these operations with integral operands.
SIDEBAR: Multiplication produces a double length product, and the programmer must remember that discarding
the low order part of a double length negative leaves the high order part in correct twos complement form
only if the low order part is null.
...
2.5 FIXED POINT ARITHMETIC
For fixed point arithmetic the PDP-10 has instructions for arithmetic shifting (which is essentially
multiplication by a power of 2) as well as for performing addition, subtraction, multiplication and division
of numbers in fixed point format [§ 1.1]. In such numbers the position of the binary point is arbitrary
(the programmer may adopt any point convention). The add and subtract instructions involve only single length
numbers, whereas multiply supplies a double length product, and divide uses a double length dividend. The high
and low order words respectively of a double length fixed point number are in accumulators A and A+1 (mod 20),
where the magnitude is the 70-bit string in bits 1-35 of the two words and the signs of the two are identical.
There are also integer multiply and divide instructions that involve only single length numbers and are
especially suited for handling smaller integers, particularly those of eighteen bits or less such as addresses
(of course they can be used for small fractions as well provided the programmer keeps track of the binary point).
For convenience in the following, all operands are assumed to be integers (binary point at the right).
The processor has four flags, Overflow, Carry 0, Carry 1 and No Divide, that indicate when the magnitude of a
number is or would be larger than can be accommodated. Carry 0 and Carry 1 actually detect carries out of bits
0 and 1 in certain instructions that employ fixed point arithmetic operations: the add and subtract instructions
treated here, the move instructions that produce the negative or magnitude of the word moved [§ 2.2], and the
arithmetic test instructions that increment or decrement the test word [§ 2.7]. In these instructions an
incorrect result is indicated - and the Overflow flag set - if the carries are different, ie if there is a carry
into the sign but not out of it, or vice versa. The Overflow flag is also set by No Divide being set, which
means the processor has failed to perform a division because the magnitude of the dividend is greater than or
equal to that of the divisor, or in integer divide, simply that the divisor is zero. In other overflow cases
only Overflow itself is set: these include too large a product in multiplication, and loss of significant bits
in left arithmetic shifting.
SIDEBAR: Overflow is determined directly from the carries, not from the carry flags, as their states may reflect
events in previous instructions.
*/
/**
* opKA10(op)
*
* Originally, we received the full opcode (36 bits), then only the upper half-word (18 bits),
* and now we receive only the upper 13 bits. However, the octal values shown in the table and
* function comments below still include all 18 bits of the original upper half-word, so that
* you don't have to mentally un-shift them 5 bits.
*
* @this {CPUStatePDP10}
* @param {number} op (the top 13 bits of the original opcode, shifted right to bit 0)
*/
PDP10.opKA10 = function(op)
{
/*
* We shift op right 4 more bits, leaving only the 9 bits required for the table index. Those
* 4 bits are normally an accumulator index, which we also mask and pass along, since most instructions
* will use an accumulator, and those that don't simply ignore it.
*/
PDP10.aOpXXX_KA10[op >> 4].call(this, op, op & PDP10.OPCODE.A_MASK);
};
/**
* opUUO(0o0NN000): Unimplemented User Operation
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-64:
*
* Store the instruction code, A and the effective address E in bits 0-8, 9-12 and 18-35 respectively of
* location 40; clear bits 13-17. Execute the instruction contained in location 41. The original contents
* of location 40 are lost.
*
* All of these codes are equivalent when they occur in the Monitor or when time sharing is not in effect.
* But when a UUO appears in a user program, a code in the range 001-037 uses relocated locations 40 and 41
* (ie 40 and 41 in the user's block) and is thus entirely a part of and under control of the user program.
*
* A code in the range 040-077 on the other hand uses unrelocated 40 and 41, and the instruction in the latter
* location is under control of the Monitor; these codes are thus specifically for user communication with
* the Monitor, which interprets them (refer to the Monitor manual for the meanings of the various codes).
*
* The code 000 executes in the same way as 040-077 but is not a standard communication code: it is included
* so that control returns to the Monitor should a user program wipe itself out.
*
* For a second processor connected to the same memory, the UUO trap is locations 140-141 instead of 40-41.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opUUO = function(op, ac)
{
this.opUndefined(op);
};
/**
* opUFA(0o130000): Unnormalized Floating Add
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-37:
*
* Floating add the contents of location E to AC. If the double length fraction in the sum is zero, clear
* accumulator A+1. Otherwise normalize the sum only if the magnitude of its fractional part is >= 1, and place
* the high order part of the result in AC A+1. The original contents of AC and E are unaffected.
*
* NOTE: The result is placed in accumulator A+1. T his is the only arithmetic instruction that stores the result
* in a second accumulator, leaving the original operands intact.
*
* If the exponent of the sum following the one-step normalization is > 127, set Overflow and Floating Overflow;
* the result stored has an exponent 256 less than the correct one.
*
* SIDEBAR: The exponent of the sum is equal to that of the larger summand unless addition of the fractions
* overflows, in which case it is greater by 1. Exponent overflow can occur only in the latter case.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opUFA = function(op, ac)
{
this.opUndefined(op);
};
/**
* opDFN(0o131000): Double Floating Negate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-37:
*
* Negate the double length floating point number composed of the contents of AC and location E with AC on the left.
* Do this by taking the twos complement of the number whose sign is AC bit 0, whose exponent is in AC bits 1-8, and
* whose fraction is the 54-bit string in bits 9-35 of AC and location E. Place the high order word of the result
* in AC; place the low order part of the fraction in bits 9-35 of location E without altering the original contents
* of bits 0-8 of that location.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opDFN = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFSC(0o132000): Floating Scale
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-34:
*
* If the fractional part of AC is zero, clear AC. Otherwise add the scale factor given by E to the exponent part
* of AC (thus multiplying AC by 2^E), normalize the resulting word bringing 0s into bit positions vacated at the
* right, and place the result back in AC.
*
* NOTE: A negative E is represented in standard twos complement notation, but the hardware compensates for this
* when scaling the exponent.
*
* If the exponent after normalization is > 127, set Overflow and Floating Overflow; the result stored has an
* exponent 256 less than the correct one. If < -128, set Overflow, Floating Overflow and Floating Underflow;
* the result stored has an exponent 256 greater than the correct one.
*
* SIDEBAR: This instruction can be used to float a fixed number with 27 or fewer significant bits. To float an
* integer contained within AC bits 9-35,
*
* FSC AC,233
*
* inserts the correct exponent to move the binary point from the right end to the left of bit 9 and then normalizes
* (233 [base 8] = 155 [base 10] = 128 + 27).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFSC = function(op, ac)
{
this.opUndefined(op);
};
/**
* opIBP(0o133000): Increment Byte Pointer
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-16:
*
* Increment the byte pointer in location E as explained [below]. The pointer has the format:
*
* 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
* P P P P P P S S S S S S - I X X X X Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
*
* where S is the size of the byte as a number of bits, and P is its position as the number of bits
* remaining at the right of the byte in the word (eg if P is 3 the rightmost bit of the byte is bit 32
* of the word). The rest of the pointer is interpreted in the same way as in an instruction: I, X and Y
* are used to calculate the address of the location that is the source or destination of the byte.
*
* To facilitate processing a series of bytes, several of the byte instructions increment the
* pointer, ie, modify it so that it points to the next byte position in a set of memory locations.
* Bytes are processed from left to right in a word, so incrementing merely replaces the current value
* of P by P - S, unless there is insufficient space in the present location for another byte of the
* specified size (P - S < 0). In this case Y is increased by one to point to the next consecutive
* location, and P is set to 36 - S to point to the first byte at the left in the new location.
*
* CAUTION: Do not allow Y to reach maximum value. The whole pointer is incremented, so if Y is 2^18 - 1
* it becomes zero and X is also incremented. The address calculation for the pointer uses the original X,
* but if a priority interrupt should occur before the calculation is complete, the incremented X is used
* when the instruction is repeated.
*
* SPECIAL CONSIDERATIONS: If S is greater than P and also greater than 36, incrementing produces a new
* P equal to 100 - S rather than 36 - S. For S > 36 the byte is at most the entire word; for P >= 36 no
* byte is processed (loading merely clears AC). If both P and S are less than 36 but P + S > 36,
* a byte of size 36 - P is loaded from position P, or the right 36 - P bits of the byte are deposited
* in position P.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIBP = function(op, ac)
{
var inc = 0;
var w = this.readWord(this.regEA);
var p = (w / PDP10.OPCODE.P_SCALE) & PDP10.OPCODE.P_MASK;
var s = (w >> PDP10.OPCODE.S_SHIFT) & PDP10.OPCODE.S_MASK;
p -= s;
if (p < 0) {
inc++;
p = 36 - s;
if (p < 0) p = 100 - s; // see SPECIAL CONSIDERATIONS and NOTE above
}
/*
* Since the documentation above makes clear that the effect of the increment (of w) extends past the Y
* bits and into (at least) the X bits, we first re-assemble a new pointer with updated P bits (along with
* the original S bits and the remaining bits covered by PTR_MASK) and then increment as needed.
*
* Yes, PTR_MASK could have included the S bits as well and made the following expression a TINY bit simpler,
* but I would like to keep PTR_MASK distinct from both the P and S fields.
*/
w = (p * PDP10.OPCODE.P_SCALE) + (s << PDP10.OPCODE.S_SHIFT) + (w & PDP10.OPCODE.PTR_MASK);
if (inc) w = (w + inc) % PDP10.WORD_LIMIT;
this.writeWord(this.regEA, w);
};
/**
* opILDB(0o134000): Increment Pointer and Load Byte
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-16:
*
* Increment the byte pointer in location E as explained above. Then retrieve a byte of S bits from the
* location and position specified by the newly incremented pointer, load it into the right end of AC,
* and clear the remaining AC bits. The location containing the byte is unaffected, the original contents
* of AC are lost.
*
* SPECIAL CONSIDERATIONS: If S is greater than P and also greater than 36, incrementing produces a new
* P equal to 100 - S rather than 36 - S. For S > 36 the byte is at most the entire word; for P >= 36 no
* byte is processed (loading merely clears AC). If both P and S are less than 36 but P + S > 36,
* a byte of size 36 - P is loaded from position P, or the right 36 - P bits of the byte are deposited
* in position P.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opILDB = function(op, ac)
{
/*
* We're called in two phases: phase 1 is with regEA containing the address of the pointer, and phase 2
* is with regEA containing the address of the bits to be loaded.
*
* We can implement this as a simple combination of opIBP() and opLDB(), but taking care that we only call
* opIBP() on phase 1 (and only if the BIS flag is not set).
*/
if (!(this.regPS & PDP10.PSFLAG.BIS)) {
PDP10.opIBP.call(this, op, ac);
this.regPS |= PDP10.PSFLAG.BIS;
}
PDP10.opLDB.call(this, op, ac);
};
/**
* opLDB(0o135000): Load Byte
*z
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-16:
*
* Retrieve a byte of S bits from the location and position specified by the pointer contained in location E,
* load it into the right end of AC, and clear the remaining AC bits. The location containing the byte is
* unaffected, the original contents of AC are lost.
*
* SPECIAL CONSIDERATIONS: If S is greater than P and also greater than 36, incrementing produces a new
* P equal to 100 - S rather than 36 - S. For S > 36 the byte is at most the entire word; for P >= 36 no
* byte is processed (loading merely clears AC). If both P and S are less than 36 but P + S > 36,
* a byte of size 36 - P is loaded from position P, or the right 36 - P bits of the byte are deposited
* in position P.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opLDB = function(op, ac)
{
/*
* We're called in two phases: phase 1 is with regEA containing the address of the pointer, and phase 2
* is with regEA containing the address of the bits to be loaded.
*/
var w = this.readWord(this.regEA);
if (this.regBP < 0) {
this.regBP = w;
this.regRA = this.regEA | PDP10.OPCODE.I_FIELD;
return;
}
var p = (this.regBP / PDP10.OPCODE.P_SCALE) & PDP10.OPCODE.P_MASK;
var s = (this.regBP >> PDP10.OPCODE.S_SHIFT) & PDP10.OPCODE.S_MASK;
if (p + s < 32) {
/*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*/
w = ((w >> p) & ((1 << s) - 1)) >>> 0;
} else {
/*
* Regarding the SPECIAL CONSIDERATIONS above, even if the P ("shift") and/or S ("mask") values are
* over-large, that should be fine, because nothing but additional zero bits will be included (provided
* our memory is working properly and didn't give us more than 36 bits of unsigned data).
*/
w = Math.trunc(w / Math.pow(2, p)) % Math.pow(2, s);
}
this.writeWord(ac, w);
this.regPS &= ~PDP10.PSFLAG.BIS;
this.regBP = -1;
};
/**
* opIDPB(0o136000): Increment Pointer and Deposit Byte
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-16:
*
* Increment the byte pointer in location E as explained above. Then deposit the right S bits of AC into
* the location and position specified by the newly incremented pointer. The original contents of the bits
* that receive the byte are lost, AC and the remaining bits of the deposit location are unaffected.
*
* SPECIAL CONSIDERATIONS: If S is greater than P and also greater than 36, incrementing produces a new
* P equal to 100 - S rather than 36 - S. For S > 36 the byte is at most the entire word; for P >= 36 no
* byte is processed (loading merely clears AC). If both P and S are less than 36 but P + S > 36,
* a byte of size 36 - P is loaded from position P, or the right 36 - P bits of the byte are deposited
* in position P.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIDPB = function(op, ac)
{
/*
* We're called in two phases: phase 1 is with regEA containing the address of the pointer, and phase 2
* is with regEA containing the address of the bits to be stored.
*
* We can implement this as a simple combination of opIBP() and opDDB(), but taking care that we only call
* opIBP() on phase 1 (and only if the BIS flag is not set).
*/
if (!(this.regPS & PDP10.PSFLAG.BIS)) {
PDP10.opIBP.call(this, op, ac);
this.regPS |= PDP10.PSFLAG.BIS;
}
PDP10.opDPB.call(this, op, ac);
};
/**
* opDPB(0o137000): Deposit Byte
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-16:
*
* Deposit the right S bits of AC into the location and position specified by the pointer contained
* in location E. The original contents of the bits that receive the byte are lost, AC and the remaining
* bits of the deposit location are unaffected.
*
* SPECIAL CONSIDERATIONS: If S is greater than P and also greater than 36, incrementing produces a new
* P equal to 100 - S rather than 36 - S. For S > 36 the byte is at most the entire word; for P >= 36 no
* byte is processed (loading merely clears AC). If both P and S are less than 36 but P + S > 36,
* a byte of size 36 - P is loaded from position P, or the right 36 - P bits of the byte are deposited
* in position P.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opDPB = function(op, ac)
{
/*
* We're called in two phases: phase 1 is with regEA containing the address of the pointer, and phase 2
* is with regEA containing the address of the bits to be stored.
*/
var w = this.readWord(this.regEA);
if (this.regBP < 0) {
this.regBP = w;
this.regRA = this.regEA | PDP10.OPCODE.I_FIELD;
return;
}
var p = (this.regBP / PDP10.OPCODE.P_SCALE) & PDP10.OPCODE.P_MASK;
var s = (this.regBP >> PDP10.OPCODE.S_SHIFT) & PDP10.OPCODE.S_MASK;
/*
* Regarding the SPECIAL CONSIDERATIONS above, even if the P ("shift") and/or S ("mask") values are
* over-large, we "mask" the resulting byte value (b) to 36 bits, so that when we re-assemble the final
* result (w), there shouldn't be any overlap or overflow.
*/
var b = ((this.readWord(ac) % Math.pow(2, s)) * Math.pow(2, p)) % PDP10.WORD_LIMIT;
w = (w - (w % Math.pow(2, p + s))) + b + (w % Math.pow(2, p));
this.writeWord(this.regEA, w);
this.regPS &= ~PDP10.PSFLAG.BIS;
this.regBP = -1;
};
/**
* opFAD(0o140000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFAD = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFADI(0o141000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFADI = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFADM(0o142000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFADM = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFADB(0o143000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFADB = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFADR(0o144000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFADR = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFADRI(0o145000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFADRI = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFADRM(0o146000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFADRM = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFADRB(0o147000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFADRB = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFSB(0o150000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFSB = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFSBI(0o151000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFSBI = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFSBM(0o152000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFSBM = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFSBB(0o153000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFSBB = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFSBR(0o154000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFSBR = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFSBRI(0o155000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFSBRI = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFSBRM(0o156000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFSBRM = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFSBRB(0o157000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFSBRB = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFMP(0o160000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFMP = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFMPI(0o161000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFMPI = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFMPM(0o162000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFMPM = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFMPB(0o163000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFMPB = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFMPR(0o164000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFMPR = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFMPRI(0o165000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFMPRI = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFMPRM(0o166000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFMPRM = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFMPRB(0o167000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFMPRB = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFDV(0o170000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFDV = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFDVI(0o171000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFDVI = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFDVM(0o172000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFDVM = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFDVB(0o173000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFDVB = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFDVR(0o174000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFDVR = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFDVRI(0o175000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFDVRI = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFDVRM(0o176000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFDVRM = function(op, ac)
{
this.opUndefined(op);
};
/**
* opFDVRB(0o177000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opFDVRB = function(op, ac)
{
this.opUndefined(op);
};
/**
* opMOVE(0o200000): Move
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Move one word from the source to the destination specified by M. The source is unaffected,
* the original contents of the destination are lost.
*
* NOTE: This is a "Basic" mode instruction: the source is [E] and the destination is [A] (opposite of "Memory").
*
* Equivalents: opSETM(0o414000) and opSETMB(0o417000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVE = function(op, ac)
{
this.writeWord(ac, this.readWord(this.regEA));
};
/**
* opMOVEI(0o201000): Move Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Move one word from the source to the destination specified by M. The source is unaffected,
* the original contents of the destination are lost.
*
* SIDEBAR: MOVEI loads the word 0,E into AC and is thus equivalent to HRRZI.
*
* NOTE: This is an "Immediate" mode instruction: the source is the word 0,E and the destination is [A].
*
* Equivalents: opSETMI(0o415000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVEI = function(op, ac)
{
this.writeWord(ac, this.regEA);
};
/**
* opMOVEM(0o202000): Move to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Move one word from the source to the destination specified by M. The source is unaffected,
* the original contents of the destination are lost.
*
* NOTE: This is a "Memory" mode instruction: the source is [A] and the destination is [E] (opposite of "Basic").
*
* Equivalents: opSETAM(0o426000) and opSETAB(0o427000).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVEM = function(op, ac)
{
this.writeWord(this.regEA, this.readWord(ac));
};
/**
* opMOVES(0o203000): Move to Self
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Move one word from the source to the destination specified by M. The source is unaffected,
* the original contents of the destination are lost.
*
* SIDEBAR: If A is zero, MOVES is a no-op; otherwise it is equivalent to MOVE.
*
* NOTE: This is a "Self" mode instruction: the source AND destination is [E] (and also [A] if A is non-zero).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVES = function(op, ac)
{
if (ac) this.writeWord(ac, this.readWord(this.regEA));
};
/**
* opMOVS(0o204000): Move Swapped
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Interchange the left and right halves of the word from the source specified by M and move it to the
* specified destination. The source is unaffected, the original contents of the destination are lost.
*
* NOTE: This is a "Basic" mode instruction: the source is [E] and the destination is [A] (opposite of "Memory").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVS = function(op, ac)
{
var src = this.readWord(this.regEA);
src = ((src / PDP10.HALF_SHIFT)|0) + ((src & PDP10.HALF_MASK) * PDP10.HALF_SHIFT);
this.writeWord(ac, src);
};
/**
* opMOVSI(0o205000): Move Swapped Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Interchange the left and right halves of the word from the source specified by M and move it to the
* specified destination. The source is unaffected, the original contents of the destination are lost.
*
* SIDEBAR: Swapping halves in immediate mode loads the word E,0 into AC. MOVSI is thus equivalent to HRLZI.
*
* NOTE: This is an "Immediate" mode instruction: the source is the word 0,E and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVSI = function(op, ac)
{
this.writeWord(ac, this.regEA * PDP10.HALF_SHIFT);
};
/**
* opMOVSM(0o206000): Move Swapped to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Interchange the left and right halves of the word from the source specified by M and move it to the
* specified destination. The source is unaffected, the original contents of the destination are lost.
*
* NOTE: This is a "Memory" mode instruction: the source is [A] and the destination is [E] (opposite of "Basic").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVSM = function(op, ac)
{
var src = this.readWord(ac);
src = ((src / PDP10.HALF_SHIFT)|0) + ((src & PDP10.HALF_MASK) * PDP10.HALF_SHIFT);
this.writeWord(this.regEA, src);
};
/**
* opMOVSS(0o207000): Move Swapped to Self
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Interchange the left and right halves of the word from the source specified by M and move it to the
* specified destination. The source is unaffected, the original contents of the destination are lost.
*
* NOTE: This is a "Self" mode instruction: the source AND destination is [E] (and also [A] if A is non-zero).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVSS = function(op, ac)
{
var src = this.readWord(this.regEA);
src = ((src / PDP10.HALF_SHIFT)|0) + ((src & PDP10.HALF_MASK) * PDP10.HALF_SHIFT);
this.writeWord(this.regEA, src);
if (ac) this.writeWord(ac, src)
};
/**
* opMOVN(0o210000): Move Negative
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Negate the word from the source specified by M and move it to the specified destination.
* If the source word is fixed point -2^35 (400000 000000) set the Overflow and Carry 1 flags.
*
* (Negating the equivalent floating point -1 * 2^127 sets the flags, but this is not a normalized
* number).
*
* If the source word is zero, set Carry 0 and Carry 1. The source is unaffected, the original
* contents of the destination are lost.
*
* NOTE: This is a "Basic" mode instruction: the source is [E] and the destination is [A] (opposite of "Memory").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVN = function(op, ac)
{
this.writeWord(ac, PDP10.doNEG.call(this, this.readWord(this.regEA)));
};
/**
* opMOVNI(0o211000): Move Negative Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Negate the word from the source specified by M and move it to the specified destination.
* If the source word is fixed point -2^35 (400000 000000) set the Overflow and Carry 1 flags.
*
* (Negating the equivalent floating point -1 * 2^127 sets the flags, but this is not a normalized
* number).
*
* If the source word is zero, set Carry 0 and Carry 1. The source is unaffected, the original
* contents of the destination are lost.
*
* SIDEBAR: MOVNI loads AC with the negative of the word 0,E and can set no flags.
*
* NOTE: This is an "Immediate" mode instruction: the source is the word 0,E and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVNI = function(op, ac)
{
/*
* We used to perform an in-line two's complement of regEA, since doNEG() updates the flags, and the
* documentation above claimed that MOVNI must "set no flags." It's certainly true that regEA, being an
* 18-bit value, could never be -2^35, but it COULD be zero. However, SIMH doesn't treat zero any
* differently for MOVNI, so we currently don't either.
*
* TODO: Verify the "set no flags" assertion on *real* (KA10) hardware.
*/
this.writeWord(ac, PDP10.doNEG.call(this, this.regEA) /* this.regEA? PDP10.TWO_POW36 - this.regEA : 0 */);
};
/**
* opMOVNM(0o212000): Move Negative to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Negate the word from the source specified by M and move it to the specified destination.
* If the source word is fixed point -2^35 (400000 000000) set the Overflow and Carry 1 flags.
*
* (Negating the equivalent floating point -1 * 2^127 sets the flags, but this is not a normalized
* number).
*
* If the source word is zero, set Carry 0 and Carry 1. The source is unaffected, the original
* contents of the destination are lost.
*
* NOTE: This is a "Memory" mode instruction: the source is [A] and the destination is [E] (opposite of "Basic").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVNM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.doNEG.call(this, this.readWord(ac)));
};
/**
* opMOVNS(0o213000): Move Negative to Self
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-11:
*
* Negate the word from the source specified by M and move it to the specified destination.
* If the source word is fixed point -2^35 (400000 000000) set the Overflow and Carry 1 flags.
*
* (Negating the equivalent floating point -1 * 2^127 sets the flags, but this is not a normalized
* number).
*
* If the source word is zero, set Carry 0 and Carry 1. The source is unaffected, the original
* contents of the destination are lost.
*
* NOTE: This is a "Self" mode instruction: the source AND destination is [E] (and also [A] if A is non-zero).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVNS = function(op, ac)
{
var dst = PDP10.doNEG.call(this, this.readWord(this.regEA));
this.writeWord(this.regEA, dst);
if (ac) this.writeWord(ac, dst);
};
/**
* opMOVM(0o214000): Move Magnitude
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-12:
*
* Take the magnitude of the word contained in the source specified by M and move it to the
* specified destination. If the source word is fixed point -2^35 (400000 000000) set the
* Overflow and Carry 1 flags.
*
* (Negating the equivalent floating point -1 * 2^27 sets the flags, but this is not a normalized
* number).
*
* The source is unaffected, the original contents of the destination are lost.
*
* NOTE: This is a "Basic" mode instruction: the source is [E] and the destination is [A] (opposite of "Memory").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVM = function(op, ac)
{
this.writeWord(ac, PDP10.doABS.call(this, this.readWord(this.regEA)));
};
/**
* opMOVMI(0o215000): Move Magnitude Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-12:
*
* Take the magnitude of the word contained in the source specified by M and move it to the
* specified destination. If the source word is fixed point -2^35 (400000 000000) set the
* Overflow and Carry 1 flags.
*
* (Negating the equivalent floating point -1 * 2^27 sets the flags, but this is not a normalized
* number).
*
* The source is unaffected, the original contents of the destination are lost.
*
* SIDEBAR: The word 0,E is equivalent to its magnitude, so MOVMI is equivalent to MOVEI.
*
* NOTE: This is an "Immediate" mode instruction: the source is the word 0,E and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVMI = function(op, ac)
{
this.writeWord(ac, this.regEA); // src is an 18-bit immediate value, so there's no need to call doABS()
};
/**
* opMOVMM(0o216000): Move Magnitude to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-12:
*
* Take the magnitude of the word contained in the source specified by M and move it to the
* specified destination. If the source word is fixed point -2^35 (400000 000000) set the
* Overflow and Carry 1 flags.
*
* (Negating the equivalent floating point -1 * 2^27 sets the flags, but this is not a normalized
* number).
*
* The source is unaffected, the original contents of the destination are lost.
*
* NOTE: This is a "Memory" mode instruction: the source is [A] and the destination is [E] (opposite of "Basic").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVMM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.doABS.call(this, this.readWord(ac)));
};
/**
* opMOVMS(0o217000): Move Magnitude to Self
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-12:
*
* Take the magnitude of the word contained in the source specified by M and move it to the
* specified destination. If the source word is fixed point -2^35 (400000 000000) set the
* Overflow and Carry 1 flags.
*
* (Negating the equivalent floating point -1 * 2^27 sets the flags, but this is not a normalized
* number).
*
* The source is unaffected, the original contents of the destination are lost.
*
* NOTE: This is a "Self" mode instruction: the source AND destination is [E] (and also [A] if A is non-zero).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMOVMS = function(op, ac)
{
var dst = PDP10.doABS.call(this, this.readWord(this.regEA));
this.writeWord(this.regEA, dst);
if (ac) this.writeWord(ac, dst);
};
/**
* opIMUL(0o220000): Integer Multiply
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-28:
*
* Multiply AC by the operand specified by M, and place the sign and the 35 low order magnitude bits of the
* product in the specified destination. Set Overflow if the product is >= 2^35 or < -2^35 (ie if the high order
* word of the double length product is not null); the high order word is lost.
*
* NOTE: This is a "Basic" mode instruction: the source is [E] and the destination is [A] (opposite of "Memory").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIMUL = function(op, ac)
{
this.writeWord(ac, PDP10.doMUL.call(this, this.readWord(ac), this.readWord(this.regEA), true));
};
/**
* opIMULI(0o221000): Integer Multiply Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-28:
*
* Multiply AC by the operand specified by M, and place the sign and the 35 low order magnitude bits of the
* product in the specified destination. Set Overflow if the product is >= 2^35 or < -2^35 (ie if the high order
* word of the double length product is not null); the high order word is lost.
*
* NOTE: This is an "Immediate" mode instruction: the source is the word 0,E and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIMULI = function(op, ac)
{
this.writeWord(ac, PDP10.doMUL.call(this, this.readWord(ac), this.regEA, true));
};
/**
* opIMULM(0o222000): Integer Multiply to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-28:
*
* Multiply AC by the operand specified by M, and place the sign and the 35 low order magnitude bits of the
* product in the specified destination. Set Overflow if the product is >= 2^35 or < -2^35 (ie if the high order
* word of the double length product is not null); the high order word is lost.
*
* NOTE: This is a "Memory" mode instruction: the source is [E] and the destination is [E] (opposite of "Basic").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIMULM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.doMUL.call(this, this.readWord(ac), this.readWord(this.regEA), true));
};
/**
* opIMULB(0o223000): Integer Multiply to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-28:
*
* Multiply AC by the operand specified by M, and place the sign and the 35 low order magnitude bits of the
* product in the specified destination. Set Overflow if the product is >= 2^35 or < -2^35 (ie if the high order
* word of the double length product is not null); the high order word is lost.
*
* NOTE: This is a "Both" mode instruction: the source is [E] and the destination is [E] and [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIMULB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.doMUL.call(this, this.readWord(ac), this.readWord(this.regEA), true)));
};
/**
* opMUL(0o224000): Multiply
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-28:
*
* Multiply AC by the operand specified by M, and place the high order word of the double length result
* in the specified destination. If M specifies AC as a destination, place the low order word in accumulator
* A+1. If both operands are -2^35 set Overflow; the double length result stored is -2^70.
*
* NOTE: This is a "Basic" mode instruction: the source is [E] and the destination is [A],[A+1] (opposite of "Memory").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMUL = function(op, ac)
{
this.writeWord(ac, PDP10.doMUL.call(this, this.readWord(ac), this.readWord(this.regEA)));
this.writeWord((ac + 1) & 0o17, this.regEX);
};
/**
* opMULI(0o225000): Multiply Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-28:
*
* Multiply AC by the operand specified by M, and place the high order word of the double length result
* in the specified destination. If M specifies AC as a destination, place the low order word in accumulator
* A+1. If both operands are -2^35 set Overflow; the double length result stored is -2^70.
*
* NOTE: This is an "Immediate" mode instruction: the source is the word 0,E and the destination is [A],[A+1].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMULI = function(op, ac)
{
this.writeWord(ac, PDP10.doMUL.call(this, this.readWord(ac), this.regEA));
this.writeWord((ac + 1) & 0o17, this.regEX);
};
/**
* opMULM(0o226000): Multiply to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-28:
*
* Multiply AC by the operand specified by M, and place the high order word of the double length result
* in the specified destination. If M specifies AC as a destination, place the low order word in accumulator
* A+1. If both operands are -2^35 set Overflow; the double length result stored is -2^70.
*
* NOTE: This is a "Memory" mode instruction: the source is [E] and the destination is [E] (opposite of "Basic").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMULM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.doMUL.call(this, this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opMULB(0o227000): Multiply to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-28:
*
* Multiply AC by the operand specified by M, and place the high order word of the double length result
* in the specified destination. If M specifies AC as a destination, place the low order word in accumulator
* A+1. If both operands are -2^35 set Overflow; the double length result stored is -2^70.
*
* NOTE: This is a "Both" mode instruction: the source is [E] and the destination is [E] and [A],[A+1].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opMULB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.doMUL.call(this, this.readWord(ac), this.readWord(this.regEA))));
this.writeWord((ac + 1) & 0o17, this.regEX);
};
/**
* opIDIV(0o230000): Integer Divide
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-29:
*
* If the operand specified by M is zero, set Overflow and No Divide, and go immediately to the next instruction
* without affecting the original AC or memory operand in any way. Otherwise divide AC by the specified operand,
* calculating a quotient of 35 magnitude bits including leading zeros. Place the unrounded quotient in the
* specified destination. If M specifies AC as the destination, place the remainder, with the same sign as the
* dividend, in accumulator A+1.
*
* NOTE: This is a "Basic" mode instruction: the source is [E] and the destination is [A],[A+1] (opposite of "Memory").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIDIV = function(op, ac)
{
var dst = PDP10.doDIV.call(this, this.readWord(this.regEA), this.readWord(ac));
if (dst < 0) return;
this.writeWord(ac, dst);
this.writeWord((ac + 1) & 0o17, this.regEX);
};
/**
* opIDIVI(0o231000): Integer Divide Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-29:
*
* If the operand specified by M is zero, set Overflow and No Divide, and go immediately to the next instruction
* without affecting the original AC or memory operand in any way. Otherwise divide AC by the specified operand,
* calculating a quotient of 35 magnitude bits including leading zeros. Place the unrounded quotient in the
* specified destination. If M specifies AC as the destination, place the remainder, with the same sign as the
* dividend, in accumulator A+1.
*
* NOTE: This is an "Immediate" mode instruction: the source is the word 0,E and the destination is [A],[A+1].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIDIVI = function(op, ac)
{
var dst = PDP10.doDIV.call(this, this.regEA, this.readWord(ac));
if (dst < 0) return;
this.writeWord(ac, dst);
this.writeWord((ac + 1) & 0o17, this.regEX);
};
/**
* opIDIVM(0o232000): Integer Divide to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-29:
*
* If the operand specified by M is zero, set Overflow and No Divide, and go immediately to the next instruction
* without affecting the original AC or memory operand in any way. Otherwise divide AC by the specified operand,
* calculating a quotient of 35 magnitude bits including leading zeros. Place the unrounded quotient in the
* specified destination. If M specifies AC as the destination, place the remainder, with the same sign as the
* dividend, in accumulator A+1.
*
* NOTE: This is a "Memory" mode instruction: the source is [E] and the destination is [E] (opposite of "Basic").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIDIVM = function(op, ac)
{
var dst = PDP10.doDIV.call(this, this.readWord(this.regEA), this.readWord(ac));
if (dst < 0) return;
this.writeWord(this.regEA, dst);
};
/**
* opIDIVB(0o233000): Integer Divide to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-29:
*
* If the operand specified by M is zero, set Overflow and No Divide, and go immediately to the next instruction
* without affecting the original AC or memory operand in any way. Otherwise divide AC by the specified operand,
* calculating a quotient of 35 magnitude bits including leading zeros. Place the unrounded quotient in the
* specified destination. If M specifies AC as the destination, place the remainder, with the same sign as the
* dividend, in accumulator A+1.
*
* NOTE: This is a "Both" mode instruction: the source is [E] and the destination is [E] and [A],[A+1].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIDIVB = function(op, ac)
{
var dst = PDP10.doDIV.call(this, this.readWord(this.regEA), this.readWord(ac));
if (dst < 0) return;
this.writeWord(this.regEA, this.writeWord(ac, dst));
this.writeWord((ac + 1) & 0o17, this.regEX);
};
/**
* opDIV(0o234000): Divide
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-29:
*
* If the magnitude of the number in AC is greater than or equal to that of the operand specified by M,
* set Overflow and No Divide, and go immediately to the next instruction without affecting the original AC
* or memory operand in any way. Otherwise divide the double length number contained in accumulators A and A+1
* by the specified operand, calculating a quotient of 35 magnitude bits including leading zeros. Place the
* unrounded quotient in the specified destination. If M specifies AC as a destination, place the remainder,
* with the same sign as the dividend, in accumulator A+1.
*
* NOTE: This is a "Basic" mode instruction: the source is [E] and the destination is [A],[A+1] (opposite of "Memory").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opDIV = function(op, ac)
{
var ext = this.readWord(ac);
var dst = this.readWord((ac + 1) & 0o17);
dst = PDP10.doDIV.call(this, this.readWord(this.regEA), dst, ext);
if (dst < 0) return;
this.writeWord(ac, dst);
this.writeWord((ac + 1) & 0o17, this.regEX);
};
/**
* opDIVI(0o235000): Divide Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-29:
*
* If the magnitude of the number in AC is greater than or equal to that of the operand specified by M,
* set Overflow and No Divide, and go immediately to the next instruction without affecting the original AC
* or memory operand in any way. Otherwise divide the double length number contained in accumulators A and A+1
* by the specified operand, calculating a quotient of 35 magnitude bits including leading zeros. Place the
* unrounded quotient in the specified destination. If M specifies AC as a destination, place the remainder,
* with the same sign as the dividend, in accumulator A+1.
*
* NOTE: This is an "Immediate" mode instruction: the source is the word 0,E and the destination is [A],[A+1].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opDIVI = function(op, ac)
{
var ext = this.readWord(ac);
var dst = this.readWord((ac + 1) & 0o17);
dst = PDP10.doDIV.call(this, this.regEA, dst, ext);
if (dst < 0) return;
this.writeWord(ac, dst);
this.writeWord((ac + 1) & 0o17, this.regEX);
};
/**
* opDIVM(0o236000): Divide to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-29:
*
* If the magnitude of the number in AC is greater than or equal to that of the operand specified by M,
* set Overflow and No Divide, and go immediately to the next instruction without affecting the original AC
* or memory operand in any way. Otherwise divide the double length number contained in accumulators A and A+1
* by the specified operand, calculating a quotient of 35 magnitude bits including leading zeros. Place the
* unrounded quotient in the specified destination. If M specifies AC as a destination, place the remainder,
* with the same sign as the dividend, in accumulator A+1.
*
* NOTE: This is a "Memory" mode instruction: the source is [E] and the destination is [E] (opposite of "Basic").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opDIVM = function(op, ac)
{
var ext = this.readWord(ac);
var dst = this.readWord((ac + 1) & 0o17);
dst = PDP10.doDIV.call(this, this.readWord(this.regEA), dst, ext);
if (dst < 0) return;
this.writeWord(this.regEA, dst);
};
/**
* opDIVB(0o237000): Divide to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-29:
*
* If the magnitude of the number in AC is greater than or equal to that of the operand specified by M,
* set Overflow and No Divide, and go immediately to the next instruction without affecting the original AC
* or memory operand in any way. Otherwise divide the double length number contained in accumulators A and A+1
* by the specified operand, calculating a quotient of 35 magnitude bits including leading zeros. Place the
* unrounded quotient in the specified destination. If M specifies AC as a destination, place the remainder,
* with the same sign as the dividend, in accumulator A+1.
*
* NOTE: This is a "Both" mode instruction: the source is [E] and the destination is [E] and [A],[A+1].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opDIVB = function(op, ac)
{
var ext = this.readWord(ac);
var dst = this.readWord((ac + 1) & 0o17);
dst = PDP10.doDIV.call(this, this.readWord(this.regEA), dst, ext);
if (dst < 0) return;
this.writeWord(ac, this.writeWord(this.regEA, dst));
this.writeWord((ac + 1) & 0o17, this.regEX);
};
/**
* opASH(0o240000): Arithmetic Shift
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-31:
*
* Arithmetic Shifting
*
* These two instructions [ASH, ASHC] produce an arithmetic shift right or left of the number in AC
* or the double length number in accumulators A and A+1. Shifting is the movement of the contents
* of a register bit-to-bit. The operation discussed here is similar to logical shifting [see §2.4
* and the illustration on page 2-24], but in an arithmetic shift only the magnitude part is
* shifted - the sign is unaffected. In a double length number the 70-bit string made up of the
* magnitude parts of the two words is shifted, but the sign of the low order word is made equal
* to the sign of the high order word.
*
* Null bits are brought in at the end being vacated: a left shift brings in 0s at the right,
* whereas a right shift brings in the equivalent of the sign bit at the left. In either case,
* information shifted out at the other end is lost. A single shift left is equivalent to multiplying
* the number by 2 (provided no bit of significance is shifted out); a shift right divides the number
* by 2.
*
* The number of places shifted is specified by the result of the effective address calculation
* taken as a signed number (in twos complement notation) modulo 2^8 in magnitude. In other words
* the effective shift E is the number composed of bit 18 (which is the sign) and bits 28-35 of the
* calculation result. Hence the programmer may specify the shift directly in the instruction
* (perhaps indexed) or give an indirect address to be used in calculating the shift. A positive E
* produces motion to the left, a negative E to the right; E is thus the power of 2 by which the
* number is multiplied. Maximum movement is 255 places.
*
* ASH: Arithmetic Shift
*
* Shift AC arithmetically the number of places specified by E. Do not shift bit 0. If E is positive,
* shift left bringing 0s into bit 35; data shifted out of bit 1 is lost; set Overflow if any bit of
* significance is lost (a 1 in a positive number, a 0 in a negative one). If E is negative, shift right
* bringing 0s into bit 1 if AC is positive, 1s if negative; data shifted out of bit 35 is lost.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opASH = function(op, ac)
{
/*
* Convert the unsigned 18-bit value in regEA to a signed 8-bit value (-256 to 255).
*/
var s = (((this.regEA & PDP10.HINT_LIMIT) << 14) >> 23) | (this.regEA & 0xff);
if (s) {
var v , bits;
var w = this.readWord(ac);
if (s > 0) {
bits = PDP10.INT_MASK;
v = (w < PDP10.INT_LIMIT)? 0 : PDP10.INT_LIMIT;
if (s < 35) {
v += (w * Math.pow(2, s)) % PDP10.INT_LIMIT;
/*
* bits must be set to the mask of all magnitude bits shifted out of
* the original word. Using 8-bit signed words as an example, this table shows
* the bits values that would correspond to shifting 1-7 bits left:
*
* shifts bits value calculation
* ------ ----------- ------ -----------
* 1 0b01000000 128-64 128-Math.pow(2, 7-1)
* 2 0b01100000 128-32 128-Math.pow(2, 7-2)
* 3 0b01110000 128-16 128-Math.pow(2, 7-3)
* ...
* 7 0b01111111 128-1 128-Math.pow(2, 7-7)
*/
bits = PDP10.INT_LIMIT - Math.pow(2, 35 - s);
}
if (w < PDP10.INT_LIMIT) {
/*
* Since w was positive, overflow occurs ONLY if any of the bits we shifted out were 1s.
* If all those bits in the original value (w) were 0s, then adding bits to it could NOT
* produce a value > INT_MASK.
*/
if (w + bits > PDP10.INT_MASK) {
this.regPS |= PDP10.PSFLAG.AROV;
}
} else {
/*
* Since w was negative, overflow occurs ONLY if any of the bits we shifted out were 0s.
* If all those bits in the original value (w) were 1s, subtracting bits from it could NOT
* produce a value <= INT_MASK.
*/
if (w - bits < PDP10.INT_LIMIT) {
this.regPS |= PDP10.PSFLAG.AROV;
}
}
} else {
if (s <= -35) {
v = (w < PDP10.INT_LIMIT)? 0 : PDP10.INT_MASK;
} else {
v = Math.trunc(w / Math.pow(2, -s));
if (w > PDP10.INT_MASK) {
bits = PDP10.WORD_LIMIT - Math.pow(2, 36 + s);
v += bits;
}
}
}
this.writeWord(ac, v);
}
};
/**
* opROT(0o241000): Rotate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-25:
*
* Rotate AC the number of places specified by E. If E is positive, rotate left; bit 0 is rotated
* into bit 35. If E is negative, rotate right; bit 35 is rotated into bit O.
*
* The number of places moved is specified by the result of the effective address calculation taken as
* a signed number (in twos complement notation) modulo 2^8 in magnitude. In other words the effective
* shift E is the number composed of bit 18 (which is the sign) and bits 28-35 of the calculation result.
* Hence the programmer may specify the shift directly in the instruction (perhaps indexed) or give an
* indirect address to be used in calculating the shift. A positive E produces motion to the left,
* a negative E to the right; maximum movement is 255 places.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opROT = function(op, ac)
{
/*
* Convert the unsigned 18-bit value in regEA to a signed 8-bit value, modulo 36 (+/-35).
*/
var s = ((((this.regEA & PDP10.HINT_LIMIT) << 14) >> 23) | (this.regEA & 0xff)) % 36;
if (s) {
var w = this.readWord(ac);
/*
* Note that a right rotation (s < 0) of s bits is equivalent to a left rotation (s > 0) of 36 + s bits.
*/
if (s < 0) s = 36 + s;
w = ((w * Math.pow(2, s)) % PDP10.WORD_LIMIT) + Math.trunc(w / Math.pow(2, 36 - s));
this.writeWord(ac, w);
}
};
/**
* opLSH(0o242000): Logical Shift
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-24:
*
* Shift and Rotate
*
* The remaining logical instructions shift or rotate right or left the contents of AC or the contents
* of two accumulators, A and A+1 (mod 20 [base 8]), concatenated into a 72-bit register with A on the
* left. The illustration below shows the movement of information these instructions produce in the
* accumulators. In a (logical) shift the contents of a register are moved bit-to-bit with 0s brought
* in at the end being vacated; information shifted out at the other end is lost. [For a discussion of
* arithmetic shifting see § 2.5.] In rotation the contents are moved cyclically such that information
* rotated out at one end is put in at the other.
*
* The number of places moved is specified by the result of the effective address calculation taken as a
* signed number (in twos complement notation) modulo 2^8 in magnitude. In other words the effective shift
* E is the number composed of bit 18 (which is the sign) and bits 28-35 of the calculation result. Hence
* the programmer may specify the shift directly in the instruction (perhaps indexed) or give an indirect
* address to be used in calculating the shift. A positive E produces motion to the left, a negative E to
* the right; maximum movement is 255 places.
*
* LSH: Logical Shift
*
* Shift AC the number of places specified by E. If E is positive, shift left bringing 0s into bit 35;
* data shifted out of bit 0 is lost. If E is negative, shift right bringing 0s into bit 0; data shifted
* out of bit 35 is lost.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opLSH = function(op, ac)
{
/*
* Convert the unsigned 18-bit value in regEA to a signed 8-bit value (-256 to 255).
*/
var s = (((this.regEA & PDP10.HINT_LIMIT) << 14) >> 23) | (this.regEA & 0xff);
if (s) {
var w = this.readWord(ac);
if (s > 0) {
if (s >= 36) {
w = 0;
} else {
w = (w * Math.pow(2, s)) % PDP10.WORD_LIMIT;
}
} else {
if (s <= -36) {
w = 0;
} else {
w = Math.trunc(w / Math.pow(2, -s));
}
}
this.writeWord(ac, w);
}
};
/**
* opJFFO(0o243000): Jump if Find First One
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-56:
*
* If AC contains zero, clear AC A+1 and go on to the next instruction in sequence.
*
* If AC is not zero, count the number of leading 0s in it (0s to the left of the leftmost 1),
* and place the count in AC A+1. Take the next instruction from location E and continue sequential
* operation from there. In either case AC is unaffected, the original contents of AC A +1 are lost.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJFFO = function(op, ac)
{
var dst = 0;
var src = this.readWord(ac);
if (src) {
while (src < PDP10.INT_LIMIT) {
dst++;
src *= 2;
}
this.setPC(this.regEA);
}
this.writeWord((ac + 1) & 0o17, dst);
};
/**
* opASHC(0o244000): Arithmetic Shift
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-32:
*
* Concatenate the magnitude portions of accumulators A and A+1 with A on the left, and shift
* the 70-bit combination in bits 1-35 and 37-71 the number of places specified by E. Do not shift
* AC bit 0, but make bit 0 of AC A+1 equal to it if at least one shift occurs (ie if E is nonzero).
*
* If E is positive, shift left bringing 0s into bit 71 (bit 35 of AC A+1); bit 37 (bit 1 of AC A+1)
* is shifted into bit 35; data shifted out of bit 1 is lost; set Overflow if any bit of significance
* is lost (a 1 in a positive number, a 0 in a negative one). If E is negative, shift right bringing 0s
* into bit 1 if AC is positive, 1s if negative; bit 35 is shifted into bit 37; data shifted out of
* bit 71 is lost.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opASHC = function(op, ac)
{
/*
* Convert the unsigned 18-bit value in regEA to a signed 8-bit value (-256 to 255).
*/
var s = (((this.regEA & PDP10.HINT_LIMIT) << 14) >> 23) | (this.regEA & 0xff);
if (s) {
var bits;
var wLeft = this.readWord(ac);
var wRight = this.readWord((ac + 1) & 0o17);
if (s > 0) {
/*
* Handle all the left shift cases below, which don't need to worry about sign-extension
* but DO need to worry about overflow.
*/
var wLeftOrig = wLeft;
if (s >= 36) {
/*
* Since all wLeft bits are being shifted out, any positive value other than zero OR any negative value
* other than WORD_MASK indicates an overflow.
*/
if (wLeft > 0 && wLeft < PDP10.INT_LIMIT || wLeft > PDP10.INT_MASK && wLeft < PDP10.WORD_MASK) {
this.regPS |= PDP10.PSFLAG.AROV;
}
if (s >= 71) {
/*
* Since all wRight bits are being shifted out, any positive value other than zero OR any negative value
* other than WORD_MASK indicates an overflow.
*/
if (wRight > 0 && wRight < PDP10.INT_LIMIT || wRight > PDP10.INT_MASK && wRight < PDP10.WORD_MASK) {
this.regPS |= PDP10.PSFLAG.AROV;
}
wLeft = 0;
} else {
/*
* Left shift 36-70 bits.
*/
wLeft = (wRight * Math.pow(2, s - 35)) % PDP10.INT_LIMIT;
bits = PDP10.INT_LIMIT - Math.pow(2, 70 - s);
if (wLeftOrig <= PDP10.INT_MASK) {
/*
* Since wLeft was positive, overflow occurs ONLY if any of the bits we shifted out were 1s.
* If all those bits in the original value were 0s, then adding bits to it could NOT produce
* a value > INT_MASK.
*/
if (wRight + bits > PDP10.INT_MASK) {
this.regPS |= PDP10.PSFLAG.AROV;
}
} else {
/*
* Since wLeft was negative, overflow occurs ONLY if any of the bits we shifted out were 0s.
* If all those bits in the original value were 1s, subtracting bits from it could NOT produce
* a value <= INT_MASK.
*/
if (wRight - bits <= PDP10.INT_MASK) {
this.regPS |= PDP10.PSFLAG.AROV;
}
}
}
wRight = 0;
if (wLeftOrig > PDP10.INT_MASK) {
wLeft += PDP10.INT_LIMIT;
wRight += PDP10.INT_LIMIT;
}
} else {
/*
* Left shift 1-35 bits.
*/
wLeft = ((wLeft * Math.pow(2, s)) % PDP10.INT_LIMIT) + Math.trunc((wRight % PDP10.INT_LIMIT) / Math.pow(2, 35 - s));
wRight = (wRight * Math.pow(2, s)) % PDP10.INT_LIMIT;
/*
* Determine overflow and update the sign bits.
*/
bits = PDP10.INT_LIMIT - Math.pow(2, 35 - s);
if (wLeftOrig <= PDP10.INT_MASK) {
/*
* Since wLeft was positive, overflow occurs ONLY if any of the bits we shifted out were 1s.
* If all those bits in the original value were 0s, then adding bits to it could NOT produce
* a value > INT_MASK.
*/
if (wLeftOrig + bits > PDP10.INT_MASK) {
this.regPS |= PDP10.PSFLAG.AROV;
}
} else {
/*
* Since wLeft was negative, overflow occurs ONLY if any of the bits we shifted out were 0s.
* If all those bits in the original value were 1s, subtracting bits from it could NOT produce
* a value <= INT_MASK.
*/
if (wLeftOrig - bits <= PDP10.INT_MASK) {
this.regPS |= PDP10.PSFLAG.AROV;
}
/*
* Last but not least, update the sign bits of wLeft and wRight to indicate negative values.
*/
wLeft += PDP10.INT_LIMIT;
wRight += PDP10.INT_LIMIT;
}
}
} else {
/*
* Handle all the right shift cases below, which don't need to worry about overflow but DO
* need to worry about sign-extension.
*/
if (s <= -36) {
if (s <= -72) {
wRight = (wLeft > PDP10.INT_MASK? PDP10.WORD_MASK : 0);
} else {
wRight = Math.trunc((wLeft % PDP10.INT_LIMIT) / Math.pow(2, -s - 35));
}
if (wLeft <= PDP10.INT_MASK) {
wLeft = 0;
} else {
wLeft = PDP10.WORD_MASK;
wRight += PDP10.INT_LIMIT;
}
} else {
/*
* For this right shift of 1-35 bits, determine the value of bits shifted in from the left.
*/
bits = (wLeft > PDP10.INT_MASK? PDP10.WORD_LIMIT - Math.pow(2, 36 + s) : 0);
/*
* The bits that we add to wRight from wLeft must be shifted right one additional bit, because
* they must "skip over" the sign bit of wRight. This means we must zero the sign bit of wRight,
* which can be done by performing a "mod" with INT_LIMIT.
*/
wRight = Math.trunc((wRight % PDP10.INT_LIMIT) / Math.pow(2, -s)) + (((wLeft % PDP10.INT_LIMIT) * Math.pow(2, 35 + s)) % PDP10.INT_LIMIT);
wLeft = Math.trunc(wLeft / Math.pow(2, -s)) + bits;
/*
* Last but not least, we must set the sign of wRight to the sign of wLeft.
*/
if (wLeft > PDP10.INT_MASK) wRight += PDP10.INT_LIMIT;
}
}
this.writeWord(ac, wLeft);
this.writeWord((ac + 1) & 0o17, wRight);
}
};
/**
* opROTC(0o245000): Rotate Combined
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-26:
*
* Concatenate accumulators A and A+1 with A on the left, and rotate the 72-bit combination the
* number of places specified by E. If E is positive, rotate left; bit 0 is rotated into bit 71
* (bit 35 of AC A+1) and bit 36 into bit 35. If E is negative, rotate right; bit 35 is rotated
* into bit 36 and bit 71 into bit 0.
*
* The number of places moved is specified by the result of the effective address calculation taken as
* a signed number (in twos complement notation) modulo 2^8 in magnitude. In other words the effective
* shift E is the number composed of bit 18 (which is the sign) and bits 28-35 of the calculation result.
* Hence the programmer may specify the shift directly in the instruction (perhaps indexed) or give an
* indirect address to be used in calculating the shift. A positive E produces motion to the left,
* a negative E to the right; maximum movement is 255 places.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opROTC = function(op, ac)
{
/*
* Convert the unsigned 18-bit value in regEA to a signed 8-bit value, modulo 72 (+/-71).
*/
var s = ((((this.regEA & PDP10.HINT_LIMIT) << 14) >> 23) | (this.regEA & 0xff)) % 72;
if (s) {
var wLeft = this.readWord(ac);
var wRight = this.readWord((ac + 1) & 0o17);
var wLeftOrig = wLeft;
/*
* Note that a right rotation (s < 0) of s bits is equivalent to a left rotation (s > 0) of 72 + s bits.
*/
if (s < 0) s = 72 + s;
if (s < 36) {
wLeft = ((wLeft * Math.pow(2, s)) % PDP10.WORD_LIMIT) + Math.trunc(wRight / Math.pow(2, 36 - s));
wRight = ((wRight * Math.pow(2, s)) % PDP10.WORD_LIMIT) + Math.trunc(wLeftOrig / Math.pow(2, 36 - s));
} else {
wLeft = ((wRight * Math.pow(2, s - 36)) % PDP10.WORD_LIMIT) + Math.trunc(wLeft / Math.pow(2, 72 - s));
wRight = ((wLeftOrig * Math.pow(2, s - 36)) % PDP10.WORD_LIMIT) + Math.trunc(wRight / Math.pow(2, 72 - s));
}
this.writeWord(ac, wLeft);
this.writeWord((ac + 1) & 0o17, wRight);
}
};
/**
* opLSHC(0o246000): Logical Shift Combined
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-25:
*
* Concatenate accumulators A and A+1 with A on the left, and shift the 72-bit combination the number
* of places specified by E. If E is positive, shift left bringing 0s into bit 71 (bit 35 of AC A+1);
* bit 36 is shifted into bit 35; data shifted out of bit 0 is lost. If E is negative, shift right
* bringing 0s into bit 0; bit 35 is shifted into bit 36; data shifted out of bit 71 is lost.
*
* The number of places moved is specified by the result of the effective address calculation taken as
* a signed number (in twos complement notation) modulo 2^8 in magnitude. In other words the effective
* shift E is the number composed of bit 18 (which is the sign) and bits 28-35 of the calculation result.
* Hence the programmer may specify the shift directly in the instruction (perhaps indexed) or give an
* indirect address to be used in calculating the shift. A positive E produces motion to the left,
* a negative E to the right; maximum movement is 255 places.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opLSHC = function(op, ac)
{
/*
* Convert the unsigned 18-bit value in regEA to a signed 8-bit value (-256 to 255).
*/
var s = (((this.regEA & PDP10.HINT_LIMIT) << 14) >> 23) | (this.regEA & 0xff);
if (s) {
var wLeft = this.readWord(ac);
var wRight = this.readWord((ac + 1) & 0o17);
if (s > 0) {
if (s >= 36) {
if (s >= 72) {
wLeft = 0;
} else {
wLeft = (wRight * Math.pow(2, s - 36)) % PDP10.WORD_LIMIT;
}
wRight = 0;
} else {
wLeft = ((wLeft * Math.pow(2, s)) % PDP10.WORD_LIMIT) + Math.trunc(wRight / Math.pow(2, 36 - s));
wRight = (wRight * Math.pow(2, s)) % PDP10.WORD_LIMIT;
}
} else {
if (s <= -36) {
if (s <= -72) {
wRight = 0;
} else {
wRight = Math.trunc(wLeft / Math.pow(2, -s - 36));
}
wLeft = 0;
} else {
wRight = Math.trunc(wRight / Math.pow(2, -s)) + ((wLeft * Math.pow(2, 36 + s)) % PDP10.WORD_LIMIT);
wLeft = Math.trunc(wLeft / Math.pow(2, -s));
}
}
this.writeWord(ac, wLeft);
this.writeWord((ac + 1) & 0o17, wRight);
}
};
/**
* opEXCH(0o250000): Exchange
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-9:
*
* Move the contents of location E to AC and move AC to location E.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opEXCH = function(op, ac)
{
var tmp = this.readWord(ac);
this.writeWord(ac, this.readWord(this.regEA));
this.writeWord(this.regEA, tmp);
};
/**
* opBLT(0o251000): Block Transfer
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-10:
*
* Beginning at the location addressed by AC left, move words to another area of memory beginning at the
* location addressed by AC right. Continue until a word is moved to location E. The total number of words
* in the block is thus E - AC(right) + 1.
*
* CAUTION: Priority interrupts are allowed during the execution of this instruction, following the processing
* of each word. If an interrupt occurs, the BLT stores the source and destination addresses for the next word
* in AC, so when the processor restarts upon the return to the interrupted program, it actually resumes at
* the correct point within the BLT. Therefore, unless the interrupt system is inactive, A and X must not address
* the same register as this would produce a different effective address calculation upon resumption should an
* interrupt occur; and the program must not attempt to load an accumulator addressed either by A or X unless it
* is the final location being loaded. Furthermore, the program cannot assume that AC is the same after the BLT
* as it was before.
*
* TODO: Determine the logic behind SIMH's treatment of the AC register when it's part of the memory being transferred.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opBLT = function(op, ac)
{
var fDone = false, fUpdate = false;
var addrDst = this.readWord(ac);
var addrSrc = (addrDst / PDP10.HALF_SHIFT)|0;
addrDst &= PDP10.HALF_MASK;
while (!fDone) {
this.writeWord(addrDst, this.readWord(addrSrc));
/*
* NOTE: The PDP-10 specs (especially the KA10 Reference Manual) are not very clear on the exit criteria:
* the transfer stops once AC left >= E, not AC left == E. They are also not very clear on whether the addresses
* are incremented before or after the exit criteria is checked; however, the KA10 "DAKAM" diagnostic seems
* pretty adamant that, at least after a one-word BLT operation, the addresses should NOT be incremented.
*/
if (!(fDone = (addrDst >= this.regEA))) {
addrSrc = (addrSrc + 1) & PDP10.HALF_MASK;
addrDst = (addrDst + 1) & PDP10.HALF_MASK;
fUpdate = true;
}
if (fDone || !this.isRunning()) {
/*
* If the CPU isn't currently running, the CPU is presumably being stepped, so we'll treat that the
* same as the "priority interrupt" condition described above, update the addresses, rewind the PC, and leave.
*/
if (fUpdate) this.writeWord(ac, addrSrc * PDP10.HALF_SHIFT + addrDst);
if (!fDone) this.advancePC(-1);
break;
}
}
};
/**
* opAOBJP(0o252000): Add One to Both Halves of AC and Jump if Positive
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-41:
*
* Add 1000001 [base 8] to AC and place the result back in AC. If the result is greater than or equal
* to zero (ie if bit 0 is 0, and hence a negative count in the left half has reached zero or a positive
* count has not yet reached 2^17), take the next instruction from location E and continue sequential
* operation from there.
*
* The incrementing of both halves of AC simultaneously is effected by adding 1000001 [base 8]. A count
* of -2 in AC left is therefore increased to zero if 2^18 - 1 is incremented in AC right.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOBJP = function(op, ac)
{
var dst = (this.readWord(ac) + 0o000001000001) % PDP10.WORD_LIMIT;
this.writeWord(ac, dst);
if (dst < PDP10.INT_LIMIT) this.setPC(this.regEA);
};
/**
* opAOBJN(0o253000): Add One to Both Halves of AC and Jump if Negative
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-41:
*
* Add 1000001 [base 8] to AC and place the result back in AC. If the result is less than zero
* (ie if bit 0 is 1, and hence a negative count in the left half has not yet reached zero or a positive
* count has reached 2^17), take the next instruction from location E and continue sequential operation
* from there.
*
* The incrementing of both halves of AC simultaneously is effected by adding 1000001 [base 8]. A count
* of -2 in AC left is therefore increased to zero if 2^18 - 1 is incremented in AC right.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOBJN = function(op, ac)
{
var dst = (this.readWord(ac) + 0o000001000001) % PDP10.WORD_LIMIT;
this.writeWord(ac, dst);
if (dst >= PDP10.INT_LIMIT) this.setPC(this.regEA);
};
/**
* opJRST(0o254000): Jump and Restore
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-57:
*
* Perform the functions specified by F, then take the next instruction from location E and continue sequential
* operation from there. Bits 9-12 are programmed as follows.
*
* 9 Restore the channel on which the highest priority interrupt is currently being held [§ 2.13].
*
* Unless the User In-out flag is set, this function cannot be executed in a user program. Instead
* of restoring the channel, it stores its own instruction code, F and effective address E in bits 0-8,
* 9-12 and 18-35 respectively of unrelocated location 40 (clearing bits 13-17), and then executes the
* instruction contained in location 41, which is under control of the monitor [§ 2.15].
*
* 10 Halt the processor. When it stops, the MA lights on the console display an address one greater
* than that of the location containing the instruction that caused the halt, and PC displays the jump
* address (the location from which the next instruction will be taken if the operator causes the processor
* to resume operation without changing PC).
*
* Unless the User In-out flag is set, this function cannot be executed in a user program. Instead of
* halting the processor, it stores its own instruction code, F and effective address E in Bits 0-8, 9-12
* and 18-35 respectively of unrelocated location 40 (clearing bits 13-17), and then executes the
* instruction contained in location 41, which is under control of the monitor [§ 2.15].
*
* 11 Restore the flags listed above from the left half of the word in the last location referenced in the
* effective address calculation. Hence to restore flags requires that the JRST instruction use indexing
* or indirect addressing.
*
* Restoration of all but the user flags is directly according to the contents of the corresponding bits
* as given above: a flag is set by a 1 in the bit, cleared by a 0. A 1 in bit 5 sets User but a 0 has no
* effect, so the Monitor can restart a user program by restoring flags but the user cannot leave user
* mode by this method. A 0 in bit 6 clears User In-out, but a 1 sets it only if the JRST is being
* executed by the Monitor, ie if User is clear.
*
* 12 Enter user mode. The user program starts at relocated location E.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJRST = function(op, ac)
{
if (ac & 0b0001) {
/*
* Enter user mode.
*/
this.setUserMode();
}
if (ac & 0b0010) {
/*
* Restore the flags.
*/
this.setPS((this.regLA / PDP10.HALF_SHIFT)|0);
}
if (ac & 0b0100) {
/*
* Halt the processor.
*/
this.stopCPU();
}
if (ac & 0b1000) {
/*
* Restore interrupt channel.
*/
this.opUndefined(op);
}
this.setPC(this.regEA);
};
/**
* opJFCL(0o255000): Jump on Flag and Clear
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-57:
*
* If any flag specified by F [ac] is set, clear it and take the next instruction from location E,
* continuing sequential operation from there.
*
* To select one or a combination of these flags (which are among those described above) the programmer can specify
* the equivalent of an AC address that places 1s in the appropriate bits, but MACRO recognizes mnemonics for some of
* the 13-bit instruction codes (bits 0-12).
*
* JFCL JFCL 0, No-op 25500
* JOV JFCL 10, Jump on Overflow 25540
* JCRY0 JFCL 4, Jump on Carry 0 25520
* JCRY1 JFCL 2, Jump on Carry 1 25510
* JCRY JFCL 6, Jump on Carry 0 or 1 25530
* JFOV JFCL 1, Jump on Floating Overflow 25504
*
* SIDEBAR: This instruction can be used simply to clear the selected flags by having the jump address point to the
* next consecutive location, as in:
*
* JFCL 17,.+1
*
* which clears all four flags without disrupting the normal program sequence. A JFCL that selects no flag is the fastest
* no-op as it neither fetches nor stores an operand, and bits 18-35 of the instruction word can be used to store information.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJFCL = function(op, ac)
{
/*
* The ac (F) bits from the opcode align perfectly with the top 4 bits of regPS; all we have to do is shift ac left 14.
*/
var bitsPS = ac << 14;
if (this.regPS & bitsPS) {
this.regPS &= ~bitsPS;
this.setPC(this.regEA);
}
};
/**
* opXCT(0o256000): Execute
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-56:
*
* Execute the contents of location E as an instruction. Any instruction may be executed, including another XCT.
* If an XCT executes a skip instruction, the skip is relative to the location of the XCT (the first XCT if there
* are several in a chain). If an XCT executes a jump, program flow is altered as specified by the jump (no matter
* how many XCTs precede a jump instruction, when PC is saved it contains an address one greater than the location
* of the first XCT in the chain).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opXCT = function(op, ac)
{
this.regXC = this.regEA;
};
/**
* opPUSHJ(0o260000): Push Down and Jump
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-62:
*
* Add 1000001 [base 8] to AC to increment both halves by one and place the result back in AC. If the addition
* causes the count in AC left to reach zero, set the Pushdown Overflow flag. Then place the current contents of
* the flags (as described above) in the left half of the location now addressed by AC right and the contents of
* PC in the right half of that location (at this time PC contains an address one greater than the location of
* the PUSHJ instruction). Take the next instruction from location E and continue sequential operation from there.
*
* The flags are unaffected except Byte Interrupt, which is cleared. The original contents of the location added
* to the list are lost. If this instruction is executed as a result of a priority interrupt or in unrelocated
* 41 or 61 while the processor is in user mode, bit 5 of the PC word stored is 1 and the processor leaves user mode.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opPUSHJ = function(op, ac)
{
var p = (this.readWord(ac) + 0o000001000001) % PDP10.WORD_LIMIT;
this.writeWord(ac, p);
if (!((p / PDP10.HALF_SHIFT)|0)) {
this.regPS |= PDP10.PSFLAG.PDOV;
}
this.writeWord(p & PDP10.ADDR_MASK, this.getPS() * PDP10.HALF_SHIFT + this.getPC());
this.regPS &= ~PDP10.PSFLAG.BIS; // TODO: Verify that BIS is cleared AFTER calling getPS()
this.setPC(this.regEA);
};
/**
* opPUSH(0o261000): Push Down
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-13:
*
* Add 000001 000001 to AC to increment both halves by one, then move the contents of location E to the
* location now addressed by AC right. If the addition causes the count in AC left to reach zero, set the
* Pushdown Overflow flag. The contents of E are unaffected, the original contents of the location added
* to the list are lost.
*
* If we trusted SIMH, then we could not literally interpret this instruction as adding 000001 000001 to AC,
* because if AC contained 777777 777777, adding 000001 000001 would result in 000001 000000, not 000000 000000,
* and yet 000000 000000 is EXACTLY what we get in SIMH.
*
* And yet, DEC's documentation clearly contradicts what SIMH is doing:
*
* The incrementing and decrementing of both halves of AC simultaneously is effected by adding and subtracting
* 000001 000001. Hence a count of -2 in AC left is increased to zero if 2^18 - 1 is incremented in AC right,
* and conversely, 1 in AC left is decreased to -1 if zero is decremented in AC right.
*
* Perhaps SIMH is simply emulating the behavior of some later PDP-10 model, which performed independent increments?
*
* TODO: Investigate.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opPUSH = function(op, ac)
{
var p = this.readWord(ac);
if (!PDP10.SIMH) {
/*
* This is the behavior that is clearly documented by DEC.
*/
p += 0o000001000001;
this.writeWord(p & PDP10.ADDR_MASK, this.readWord(this.regEA));
if (p >= PDP10.WORD_LIMIT) p -= PDP10.WORD_LIMIT;
if (!((p / PDP10.HALF_SHIFT)|0)) {
this.regPS |= PDP10.PSFLAG.PDOV;
}
} else {
/*
* This is the SIMH behavior, which appears to increment each half of AC independently.
*/
var addr = (p + 1) & PDP10.ADDR_MASK;
this.writeWord(addr, this.readWord(this.regEA));
p = (p + 0o000001000000) - (p & PDP10.ADDR_MASK) + addr;
if (p >= PDP10.WORD_LIMIT) {
p -= PDP10.WORD_LIMIT;
this.regPS |= PDP10.PSFLAG.PDOV;
}
}
this.writeWord(ac, p);
};
/**
* opPOP(0o262000): Pop Up
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-13:
*
* Move the contents of the location addressed by AC right to location E, then subtract 000001 000001
* from AC to decrement both halves by one. If the subtraction causes the count in AC left to reach -1,
* set the Pushdown Overflow flag. The original contents of E are lost.
*
* NOTE: Because of the order in which the operands are stored, the instruction POP AC,AC would load the
* contents of the location addressed by AC right into AC on top of the pushdown count, destroying it.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opPOP = function(op, ac)
{
var p = this.readWord(ac);
var src = this.readWord(p & PDP10.ADDR_MASK);
this.writeWord(this.regEA, src);
if (this.regEA == ac) p = src; // this avoids re-reading the accumulator if the write just overwrote it
p -= 0o000001000001;
if (p < 0) p += PDP10.WORD_LIMIT;
if (((p / PDP10.HALF_SHIFT)|0) == PDP10.HALF_MASK) {
this.regPS |= PDP10.PSFLAG.PDOV;
}
this.writeWord(ac, p);
};
/**
* opPOPJ(0o263000): Pop Up and Jump
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-63:
*
* Subtract 1000001 [base 8] from AC to decrement both halves by one and place the result back in AC.
* If the subtraction causes the count in AC left to reach -1, set the Pushdown Overflow flag. Take the
* next instruction from the location addressed by the right half of the location that was addressed by
* AC right prior to the decrementing, and continue sequential operation from there.
*
* SIDEBAR: The effective address E is ignored.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opPOPJ = function(op, ac)
{
var p = this.readWord(ac);
var pc = this.readWord(p & PDP10.ADDR_MASK);
p -= 0o000001000001;
if (p < 0) p += PDP10.WORD_LIMIT;
if (((p / PDP10.HALF_SHIFT)|0) == PDP10.HALF_MASK) {
this.regPS |= PDP10.PSFLAG.PDOV;
}
this.writeWord(ac, p);
this.setPC(pc & PDP10.ADDR_MASK);
};
/**
* opJSR(0o264000): Jump to Subroutine
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-57:
*
* Place the current contents of the flags (as described above) in the left half of location E and the
* contents of PC in the right half (at this time PC contains an address one greater than the location of
* the JSR instruction). Take the next instruction from location E + 1 and continue sequential operation
* from there. The flags are unaffected except Byte Interrupt, which is cleared.
*
* If this instruction is executed as a result of a priority interrupt or in un-relocated 41 or 61 while
* the processor is in user mode, bit 5 of the PC word stored is 1 and the processor leaves user mode.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJSR = function(op, ac)
{
this.writeWord(this.regEA, this.getPS() * PDP10.HALF_SHIFT + this.getPC());
this.regPS &= ~PDP10.PSFLAG.BIS; // TODO: Verify that BIS is cleared AFTER calling getPS()
this.setPC(this.regEA + 1);
};
/**
* opJSP(0o265000): Jump and Save PC
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-58:
*
* Place the current contents of the flags (as described above) in AC left and the contents of PC in AC right
* (at this time PC contains an address one greater than the location of the JSP instruction). Take the next
* instruction from location E and continue sequential operation from there. The flags are unaffected except
* Byte Interrupt, which is cleared.
*
* If this instruction is executed as a result of a priority interrupt or in un-relocated 41 or 61 while the
* processor is in user mode, bit 5 of the PC word stored is 1 and the processor leaves user mode.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJSP = function(op, ac)
{
this.writeWord(ac, this.getPS() * PDP10.HALF_SHIFT + this.getPC());
this.regPS &= ~PDP10.PSFLAG.BIS; // TODO: Verify that BIS is cleared AFTER calling getPS()
this.setPC(this.regEA);
};
/**
* opJSA(0o266000): Jump and Save AC
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-61:
*
* Place AC in location E, the effective address E in AC left, and the contents of PC in AC right (at this time
* PC contains an address one greater than the location of the JSA instruction). Take the next instruction from
* location E + 1 and continue sequential operation from there. The original contents of E are lost.
*
* If this instruction is executed as a result of a priority interrupt or in unrelocated 41 or 61 while the processor
* is in user mode, bit 5 of the PC word stored is 1 and the processor leaves user mode.
*
* Regarding JSA and JRA:
*
* A JSA combines advantages of the JSR and JSP. JSA does modify memory, but it saves PC in an accumulator
* without losing its previous contents (at a cost of not saving the flags). It is thus convenient for multiple-entry
* subroutines. In a subroutine called by a JSR, the returning JRST must refer to the (single) entry point.
* Since a JRA can retrieve the original PC by addressing AC as an index register, it is independent of any entry point
* without tying up an accumulator to the extent a JSP would.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJSA = function(op, ac)
{
this.writeWord(this.regEA, this.readWord(ac));
this.writeWord(ac, this.regEA * PDP10.HALF_SHIFT + this.getPC());
this.setPC(this.regEA + 1);
};
/**
* opJRA(0o267000): Jump and Restore AC
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-61:
*
* Place the contents of the location addressed by AC left into AC. Take the next instruction from location E and
* continue sequential operation from there.
*
* This opcode functions as the counterpart to JSA, but ONLY if location E (the effective address calculated by the JRA) indexes
* with the same AC that was used with the JSA.
*
* JSA 17,F1
* R1: ...
* ...
* F1: 0 ; location to save AC 17, as specified by the A field of "JSA 17,F1"
* F2: ... ; first instruction executed after "JSA 17,F1"
* JRA 17,(17) ; return to R1, after restoring AC 17
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJRA = function(op, ac)
{
var acc = this.readWord(ac);
this.writeWord(ac, this.readWord((acc / PDP10.HALF_SHIFT)|0));
this.setPC(this.regEA);
};
/**
* opADD(0o270000): Add
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-27:
*
* Add the operand specified by M to AC and place the result in the specified destination. If the sum is >= 2^35
* set Overflow and Carry 1; the result stored has a minus sign but a magnitude in positive form equal to the sum less 2^35.
* If the sum is < -2^35 set Overflow and Carry 0; the result stored has a plus sign but a magnitude in negative form equal
* to the sum plus 2^35. Set both carry flags if both summands are negative, or their signs differ and their magnitudes
* are equal or the positive one is the greater in magnitude.
*
* NOTE: This is a "Basic" mode instruction: the source is [E] and the destination is [A] (opposite of "Memory").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opADD = function(op, ac)
{
this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opADDI(0o271000): Add Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-27:
*
* Add the operand specified by M to AC and place the result in the specified destination. If the sum is >= 2^35
* set Overflow and Carry 1; the result stored has a minus sign but a magnitude in positive form equal to the sum less 2^35.
* If the sum is < -2^35 set Overflow and Carry 0; the result stored has a plus sign but a magnitude in negative form equal
* to the sum plus 2^35. Set both carry flags if both summands are negative, or their signs differ and their magnitudes
* are equal or the positive one is the greater in magnitude.
*
* NOTE: This is an "Immediate" mode instruction: the source is the word 0,E and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opADDI = function(op, ac)
{
this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), this.regEA));
};
/**
* opADDM(0o272000): Add to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-27:
*
* Add the operand specified by M to AC and place the result in the specified destination. If the sum is >= 2^35
* set Overflow and Carry 1; the result stored has a minus sign but a magnitude in positive form equal to the sum less 2^35.
* If the sum is < -2^35 set Overflow and Carry 0; the result stored has a plus sign but a magnitude in negative form equal
* to the sum plus 2^35. Set both carry flags if both summands are negative, or their signs differ and their magnitudes
* are equal or the positive one is the greater in magnitude.
*
* NOTE: This is a "Memory" mode instruction: the source is [E] and the destination is [E] (opposite of "Basic").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opADDM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opADDB(0o273000): Add to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-27:
*
* Add the operand specified by M to AC and place the result in the specified destination. If the sum is >= 2^35
* set Overflow and Carry 1; the result stored has a minus sign but a magnitude in positive form equal to the sum less 2^35.
* If the sum is < -2^35 set Overflow and Carry 0; the result stored has a plus sign but a magnitude in negative form equal
* to the sum plus 2^35. Set both carry flags if both summands are negative, or their signs differ and their magnitudes
* are equal or the positive one is the greater in magnitude.
*
* NOTE: This is a "Both" mode instruction: the source is [E] and the destination is [E] and [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opADDB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), this.readWord(this.regEA))));
};
/**
* opSUB(0o274000): Subtract
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-27:
*
* Subtract the operand specified by M from AC and place the result in the specified destination. If the difference
* is >= 2^35 set Overflow and Carry 1; the result stored has a minus sign but a magnitude in positive form equal to the
* difference less 2^35. If the difference is < -2^35 set Overflow and Carry 0; the result stored has a plus sign but
* a magnitude in negative form equal to the difference plus 2^35. Set both carry flags if the signs of the operands are
* the same and AC is the greater or the two are equal, or the signs of the operands differ and AC is negative.
*
* NOTE: This is a "Basic" mode instruction: the source is [E] and the destination is [A] (opposite of "Memory").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSUB = function(op, ac)
{
this.writeWord(ac, PDP10.doSUB.call(this, this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opSUBI(0o275000): Subtract Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-27:
*
* Subtract the operand specified by M from AC and place the result in the specified destination. If the difference
* is >= 2^35 set Overflow and Carry 1; the result stored has a minus sign but a magnitude in positive form equal to the
* difference less 2^35. If the difference is < -2^35 set Overflow and Carry 0; the result stored has a plus sign but
* a magnitude in negative form equal to the difference plus 2^35. Set both carry flags if the signs of the operands are
* the same and AC is the greater or the two are equal, or the signs of the operands differ and AC is negative.
*
* NOTE: This is an "Immediate" mode instruction: the source is the word 0,E and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSUBI = function(op, ac)
{
this.writeWord(ac, PDP10.doSUB.call(this, this.readWord(ac), this.regEA));
};
/**
* opSUBM(0o276000): Subtract to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-27:
*
* Subtract the operand specified by M from AC and place the result in the specified destination. If the difference
* is >= 2^35 set Overflow and Carry 1; the result stored has a minus sign but a magnitude in positive form equal to the
* difference less 2^35. If the difference is < -2^35 set Overflow and Carry 0; the result stored has a plus sign but
* a magnitude in negative form equal to the difference plus 2^35. Set both carry flags if the signs of the operands are
* the same and AC is the greater or the two are equal, or the signs of the operands differ and AC is negative.
*
* NOTE: This is a "Memory" mode instruction: the source is [E] and the destination is [E] (opposite of "Basic").
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSUBM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.doSUB.call(this, this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opSUBB(0o277000): Subtract to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-27:
*
* Subtract the operand specified by M from AC and place the result in the specified destination. If the difference
* is >= 2^35 set Overflow and Carry 1; the result stored has a minus sign but a magnitude in positive form equal to the
* difference less 2^35. If the difference is < -2^35 set Overflow and Carry 0; the result stored has a plus sign but
* a magnitude in negative form equal to the difference plus 2^35. Set both carry flags if the signs of the operands are
* the same and AC is the greater or the two are equal, or the signs of the operands differ and AC is negative.
*
* NOTE: This is a "Both" mode instruction: the source is [E] and the destination is [E] and [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSUBB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.doSUB.call(this, this.readWord(ac), this.readWord(this.regEA))));
};
/**
* opCAIL(0o301000): Compare AC Immediate and Skip if AC Less than E
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-42:
*
* Compare AC with E (ie with the word 0,E) and skip the next instruction in sequence if the condition
* specified by M is satisfied.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAIL = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.regEA) < 0) this.setPC(this.regPC + 1);
};
/**
* opCAIE(0o302000): Compare AC Immediate and Skip if Equal
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-42:
*
* Compare AC with E (ie with the word 0,E) and skip the next instruction in sequence if the condition
* specified by M is satisfied.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAIE = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.regEA) == 0) this.setPC(this.regPC + 1);
};
/**
* opCAILE(0o303000): Compare AC Immediate and Skip if AC Less than or Equal to E
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-42:
*
* Compare AC with E (ie with the word 0,E) and skip the next instruction in sequence if the condition
* specified by M is satisfied.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAILE = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.regEA) <= 0) this.setPC(this.regPC + 1);
};
/**
* opCAIA(0o304000): Compare AC Immediate but Always Skip
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-42:
*
* Compare AC with E (ie with the word 0,E) and skip the next instruction in sequence if the condition
* specified by M is satisfied.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAIA = function(op, ac)
{
// TODO: Determine if there's any need to actually perform the comparison.
this.setPC(this.regPC + 1);
};
/**
* opCAIGE(0o305000): Compare AC Immediate and Skip if AC Greater than or Equal to E
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-42:
*
* Compare AC with E (ie with the word 0,E) and skip the next instruction in sequence if the condition
* specified by M is satisfied.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAIGE = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.regEA) >= 0) this.setPC(this.regPC + 1);
};
/**
* opCAIN(0o306000): Compare AC Immediate and Skip if Not Equal
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-42:
*
* Compare AC with E (ie with the word 0,E) and skip the next instruction in sequence if the condition
* specified by M is satisfied.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAIN = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.regEA) != 0) this.setPC(this.regPC + 1);
};
/**
* opCAIG(0o307000): Compare AC Immediate and Skip if AC Greater than E
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-42:
*
* Compare AC with E (ie with the word 0,E) and skip the next instruction in sequence if the condition
* specified by M is satisfied.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAIG = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.regEA) > 0) this.setPC(this.regPC + 1);
};
/**
* opCAML(0o311000): Compare AC with Memory and Skip if AC Less
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC with the contents of location E and skip the next instruction in sequence if the condition
* specified by M is satisfied. The pair of numbers compared may be either both fixed or both normalized
* floating point.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAML = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.readWord(this.regEA)) < 0) this.setPC(this.regPC + 1);
};
/**
* opCAME(0o312000): Compare AC with Memory and Skip if Equal
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC with the contents of location E and skip the next instruction in sequence if the condition
* specified by M is satisfied. The pair of numbers compared may be either both fixed or both normalized
* floating point.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAME = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.readWord(this.regEA)) == 0) this.setPC(this.regPC + 1);
};
/**
* opCAMLE(0o313000): Compare AC with Memory and Skip if AC Less or Equal
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC with the contents of location E and skip the next instruction in sequence if the condition
* specified by M is satisfied. The pair of numbers compared may be either both fixed or both normalized
* floating point.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAMLE = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.readWord(this.regEA)) <= 0) this.setPC(this.regPC + 1);
};
/**
* opCAMA(0o314000): Compare AC with Memory but Always Skip
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC with the contents of location E and skip the next instruction in sequence if the condition
* specified by M is satisfied. The pair of numbers compared may be either both fixed or both normalized
* floating point.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAMA = function(op, ac)
{
// TODO: Determine if there's any need to actually perform the comparison.
this.setPC(this.regPC + 1);
};
/**
* opCAMGE(0o315000): Compare AC with Memory and Skip if AC Greater or Equal
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC with the contents of location E and skip the next instruction in sequence if the condition
* specified by M is satisfied. The pair of numbers compared may be either both fixed or both normalized
* floating point.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAMGE = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.readWord(this.regEA)) >= 0) this.setPC(this.regPC + 1);
};
/**
* opCAMN(0o316000): Compare AC with Memory and Skip if Not Equal
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC with the contents of location E and skip the next instruction in sequence if the condition
* specified by M is satisfied. The pair of numbers compared may be either both fixed or both normalized
* floating point.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAMN = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.readWord(this.regEA)) != 0) this.setPC(this.regPC + 1);
};
/**
* opCAMG(0o317000): Compare AC with Memory and Skip if AC Greater
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC with the contents of location E and skip the next instruction in sequence if the condition
* specified by M is satisfied. The pair of numbers compared may be either both fixed or both normalized
* floating point.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opCAMG = function(op, ac)
{
if (PDP10.CMP(this.readWord(ac), this.readWord(this.regEA)) > 0) this.setPC(this.regPC + 1);
};
/**
* opJUMPL(0o321000): Jump if AC Less than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC (fixed or floating) with zero, and if the condition specified by M is satisfied,
* take the next instruction from location E and continue sequential operation from there.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJUMPL = function(op, ac)
{
if (PDP10.SIGN(this.readWord(ac)) < 0) this.setPC(this.regEA);
};
/**
* opJUMPE(0o322000): Jump if AC Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC (fixed or floating) with zero, and if the condition specified by M is satisfied,
* take the next instruction from location E and continue sequential operation from there.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJUMPE = function(op, ac)
{
if (PDP10.SIGN(this.readWord(ac)) == 0) this.setPC(this.regEA);
};
/**
* opJUMPLE(0o323000): Jump if AC Less than or Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC (fixed or floating) with zero, and if the condition specified by M is satisfied,
* take the next instruction from location E and continue sequential operation from there.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJUMPLE = function(op, ac)
{
if (PDP10.SIGN(this.readWord(ac)) <= 0) this.setPC(this.regEA);
};
/**
* opJUMPA(0o324000): Jump Always
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC (fixed or floating) with zero, and if the condition specified by M is satisfied,
* take the next instruction from location E and continue sequential operation from there.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJUMPA = function(op, ac)
{
this.setPC(this.regEA);
};
/**
* opJUMPGE(0o325000): Jump if AC Greater than or Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC (fixed or floating) with zero, and if the condition specified by M is satisfied,
* take the next instruction from location E and continue sequential operation from there.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJUMPGE = function(op, ac)
{
if (PDP10.SIGN(this.readWord(ac)) >= 0) this.setPC(this.regEA);
};
/**
* opJUMPN(0o326000): Jump if AC Not Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC (fixed or floating) with zero, and if the condition specified by M is satisfied,
* take the next instruction from location E and continue sequential operation from there.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJUMPN = function(op, ac)
{
if (PDP10.SIGN(this.readWord(ac)) != 0) this.setPC(this.regEA);
};
/**
* opJUMPG(0o327000): Jump if AC Greater than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-43:
*
* Compare AC (fixed or floating) with zero, and if the condition specified by M is satisfied,
* take the next instruction from location E and continue sequential operation from there.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opJUMPG = function(op, ac)
{
if (PDP10.SIGN(this.readWord(ac)) > 0) this.setPC(this.regEA);
};
/**
* opSKIP(0o330000): Do Not Skip
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Compare the contents (fixed or floating) of location E with zero, and skip the next instruction
* in sequence if the condition specified by M is satisfied. If A is nonzero also place the contents
* of location E in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSKIP = function(op, ac)
{
if (ac) this.writeWord(ac, this.readWord(this.regEA));
};
/**
* opSKIPL(0o331000): Skip if Memory Less than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Compare the contents (fixed or floating) of location E with zero, and skip the next instruction
* in sequence if the condition specified by M is satisfied. If A is nonzero also place the contents
* of location E in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSKIPL = function(op, ac)
{
var dst = this.readWord(this.regEA);
if (PDP10.SIGN(dst) < 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSKIPE(0o332000): Skip if Memory Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Compare the contents (fixed or floating) of location E with zero, and skip the next instruction
* in sequence if the condition specified by M is satisfied. If A is nonzero also place the contents
* of location E in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSKIPE = function(op, ac)
{
var dst = this.readWord(this.regEA);
if (PDP10.SIGN(dst) == 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSKIPLE(0o333000): Skip if Memory Less than or Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Compare the contents (fixed or floating) of location E with zero, and skip the next instruction
* in sequence if the condition specified by M is satisfied. If A is nonzero also place the contents
* of location E in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSKIPLE = function(op, ac)
{
var dst = this.readWord(this.regEA);
if (PDP10.SIGN(dst) <= 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSKIPA(0o334000): Skip Always
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Compare the contents (fixed or floating) of location E with zero, and skip the next instruction
* in sequence if the condition specified by M is satisfied. If A is nonzero also place the contents
* of location E in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSKIPA = function(op, ac)
{
this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, this.readWord(this.regEA));
};
/**
* opSKIPGE(0o335000): Skip if Memory Greater than or Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Compare the contents (fixed or floating) of location E with zero, and skip the next instruction
* in sequence if the condition specified by M is satisfied. If A is nonzero also place the contents
* of location E in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSKIPGE = function(op, ac)
{
var dst = this.readWord(this.regEA);
if (PDP10.SIGN(dst) >= 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSKIPN(0o336000): Skip if Memory Not Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Compare the contents (fixed or floating) of location E with zero, and skip the next instruction
* in sequence if the condition specified by M is satisfied. If A is nonzero also place the contents
* of location E in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSKIPN = function(op, ac)
{
var dst = this.readWord(this.regEA);
if (PDP10.SIGN(dst) != 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSKIPG(0o337000): Skip if Memory Greater than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Compare the contents (fixed or floating) of location E with zero, and skip the next instruction
* in sequence if the condition specified by M is satisfied. If A is nonzero also place the contents
* of location E in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSKIPG = function(op, ac)
{
var dst = this.readWord(this.regEA);
if (PDP10.SIGN(dst) > 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opAOJ(0o340000): Add One to AC but Do Not Jump
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Increment AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue
* sequential operation from there. If AC originally contained 2^35 - 1, set the Overflow and Carry 1
* flags; if -1, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOJ = function(op, ac)
{
this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), 1));
};
/**
* opAOJL(0o341000): Add One to AC and Jump if Less than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Increment AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue
* sequential operation from there. If AC originally contained 2^35 - 1, set the Overflow and Carry 1
* flags; if -1, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOJL = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), 1));
if (PDP10.SIGN(dst) < 0) this.setPC(this.regEA);
};
/**
* opAOJE(0o342000): Add One to AC and Jump if Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Increment AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue
* sequential operation from there. If AC originally contained 2^35 - 1, set the Overflow and Carry 1
* flags; if -1, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOJE = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), 1));
if (PDP10.SIGN(dst) == 0) this.setPC(this.regEA);
};
/**
* opAOJLE(0o343000): Add One to AC and Jump if Less than or Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Increment AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue
* sequential operation from there. If AC originally contained 2^35 - 1, set the Overflow and Carry 1
* flags; if -1, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOJLE = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), 1));
if (PDP10.SIGN(dst) <= 0) this.setPC(this.regEA);
};
/**
* opAOJA(0o344000): Add One to AC and Jump Always
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Increment AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue
* sequential operation from there. If AC originally contained 2^35 - 1, set the Overflow and Carry 1
* flags; if -1, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOJA = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), 1));
this.setPC(this.regEA);
};
/**
* opAOJGE(0o345000): Add One to AC and Jump if Greater than or Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Increment AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue
* sequential operation from there. If AC originally contained 2^35 - 1, set the Overflow and Carry 1
* flags; if -1, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOJGE = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), 1));
if (PDP10.SIGN(dst) >= 0) this.setPC(this.regEA);
};
/**
* opAOJN(0o346000): Add One to AC and Jump if Not Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Increment AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue
* sequential operation from there. If AC originally contained 2^35 - 1, set the Overflow and Carry 1
* flags; if -1, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOJN = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), 1));
if (PDP10.SIGN(dst) != 0) this.setPC(this.regEA);
};
/**
* opAOJG(0o347000): Add One to AC and Jump if Greater than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-44:
*
* Increment AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue
* sequential operation from there. If AC originally contained 2^35 - 1, set the Overflow and Carry 1
* flags; if -1, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOJG = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), 1));
if (PDP10.SIGN(dst) > 0) this.setPC(this.regEA);
};
/**
* opAOS(0o350000): Add One to Memory but Do Not Skip
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Increment the contents of location E by one and place the result back in E. Compare the result
* with zero, and skip the next instruction in sequence if the condition specified by M is satisfied.
* If location E originally contained 2^35 - 1, set the Overflow and Carry 1 flags; if -1, set Carry 0
* and Carry 1. If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOS = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), 1));
if (ac) this.writeWord(ac, dst);
};
/**
* opAOSL(0o351000): Add One to Memory and Skip if Less than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Increment the contents of location E by one and place the result back in E. Compare the result
* with zero, and skip the next instruction in sequence if the condition specified by M is satisfied.
* If location E originally contained 2^35 - 1, set the Overflow and Carry 1 flags; if -1, set Carry 0
* and Carry 1. If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOSL = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), 1));
if (PDP10.SIGN(dst) < 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opAOSE(0o352000): Add One to Memory and Skip if Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Increment the contents of location E by one and place the result back in E. Compare the result
* with zero, and skip the next instruction in sequence if the condition specified by M is satisfied.
* If location E originally contained 2^35 - 1, set the Overflow and Carry 1 flags; if -1, set Carry 0
* and Carry 1. If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOSE = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), 1));
if (PDP10.SIGN(dst) == 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opAOSLE(0o353000): Add One to Memory and Skip if Less than to Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Increment the contents of location E by one and place the result back in E. Compare the result
* with zero, and skip the next instruction in sequence if the condition specified by M is satisfied.
* If location E originally contained 2^35 - 1, set the Overflow and Carry 1 flags; if -1, set Carry 0
* and Carry 1. If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOSLE = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), 1));
if (PDP10.SIGN(dst) <= 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opAOSA(0o354000): Add One to Memory and Skip Always
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Increment the contents of location E by one and place the result back in E. Compare the result
* with zero, and skip the next instruction in sequence if the condition specified by M is satisfied.
* If location E originally contained 2^35 - 1, set the Overflow and Carry 1 flags; if -1, set Carry 0
* and Carry 1. If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOSA = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), 1));
this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opAOSGE(0o355000): Add One to Memory and Skip if Greater than or Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Increment the contents of location E by one and place the result back in E. Compare the result
* with zero, and skip the next instruction in sequence if the condition specified by M is satisfied.
* If location E originally contained 2^35 - 1, set the Overflow and Carry 1 flags; if -1, set Carry 0
* and Carry 1. If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOSGE = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), 1));
if (PDP10.SIGN(dst) >= 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opAOSN(0o356000): Add One to Memory and Skip if Not Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Increment the contents of location E by one and place the result back in E. Compare the result
* with zero, and skip the next instruction in sequence if the condition specified by M is satisfied.
* If location E originally contained 2^35 - 1, set the Overflow and Carry 1 flags; if -1, set Carry 0
* and Carry 1. If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOSN = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), 1));
if (PDP10.SIGN(dst) != 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opAOSG(0o357000): Add One to Memory and Skip if Greater than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Increment the contents of location E by one and place the result back in E. Compare the result
* with zero, and skip the next instruction in sequence if the condition specified by M is satisfied.
* If location E originally contained 2^35 - 1, set the Overflow and Carry 1 flags; if -1, set Carry 0
* and Carry 1. If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAOSG = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), 1));
if (PDP10.SIGN(dst) > 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSOJ(0o360000): Subtract One from AC but Do Not Jump
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Decrement AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue sequential
* operation from there. If AC originally contained - 2^35 , set the Overflow and Carry 0 flags; if any
* other nonzero number, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOJ = function(op, ac)
{
this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), PDP10.WORD_MASK));
};
/**
* opSOJL(0o361000): Subtract One from AC and Jump if Less than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Decrement AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue sequential
* operation from there. If AC originally contained - 2^35 , set the Overflow and Carry 0 flags; if any
* other nonzero number, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOJL = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) < 0) this.setPC(this.regEA);
};
/**
* opSOJE(0o362000): Subtract One from AC and Jump if Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Decrement AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue sequential
* operation from there. If AC originally contained - 2^35 , set the Overflow and Carry 0 flags; if any
* other nonzero number, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOJE = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) == 0) this.setPC(this.regEA);
};
/**
* opSOJLE(0o363000): Subtract One from AC and Jump if Less than or Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Decrement AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue sequential
* operation from there. If AC originally contained - 2^35 , set the Overflow and Carry 0 flags; if any
* other nonzero number, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOJLE = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) <= 0) this.setPC(this.regEA);
};
/**
* opSOJA(0o364000): Subtract One from AC and Jump Always
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Decrement AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue sequential
* operation from there. If AC originally contained - 2^35 , set the Overflow and Carry 0 flags; if any
* other nonzero number, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOJA = function(op, ac)
{
this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), PDP10.WORD_MASK));
this.setPC(this.regEA);
};
/**
* opSOJGE(0o365000): Subtract One from AC and Jump if Greater than or Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Decrement AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue sequential
* operation from there. If AC originally contained - 2^35 , set the Overflow and Carry 0 flags; if any
* other nonzero number, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOJGE = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) >= 0) this.setPC(this.regEA);
};
/**
* opSOJN(0o366000): Subtract One from AC and Jump if Not Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Decrement AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue sequential
* operation from there. If AC originally contained - 2^35 , set the Overflow and Carry 0 flags; if any
* other nonzero number, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOJN = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) != 0) this.setPC(this.regEA);
};
/**
* opSOJG(0o367000): Subtract One from AC and Jump if Greater than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-45:
*
* Decrement AC by one and place the result back in AC. Compare the result with zero, and if the
* condition specified by M is satisfied, take the next instruction from location E and continue sequential
* operation from there. If AC originally contained - 2^35 , set the Overflow and Carry 0 flags; if any
* other nonzero number, set Carry 0 and Carry 1.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOJG = function(op, ac)
{
var dst = this.writeWord(ac, PDP10.doADD.call(this, this.readWord(ac), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) > 0) this.setPC(this.regEA);
};
/**
* opSOS(0o370000): Subtract One from Memory but Do Not Skip
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-46:
*
* Decrement the contents of location E by one and place the result back in E. Compare the result with zero,
* and skip the next instruction in sequence if the condition specified by M is satisfied. If location E originally
* contained -2^35 , set the Overflow and Carry 0 flags; if any other nonzero number, set Carry 0 and Carry 1.
* If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOS = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), PDP10.WORD_MASK));
if (ac) this.writeWord(ac, dst);
};
/**
* opSOSL(0o371000): Subtract One from Memory and Skip if Less than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-46:
*
* Decrement the contents of location E by one and place the result back in E. Compare the result with zero,
* and skip the next instruction in sequence if the condition specified by M is satisfied. If location E originally
* contained -2^35 , set the Overflow and Carry 0 flags; if any other nonzero number, set Carry 0 and Carry 1.
* If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOSL = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) < 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSOSE(0o372000): Subtract One from Memory and Skip if Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-46:
*
* Decrement the contents of location E by one and place the result back in E. Compare the result with zero,
* and skip the next instruction in sequence if the condition specified by M is satisfied. If location E originally
* contained -2^35 , set the Overflow and Carry 0 flags; if any other nonzero number, set Carry 0 and Carry 1.
* If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOSE = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) == 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSOSLE(0o373000): Subtract One from Memory and Skip if Less than or Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-46:
*
* Decrement the contents of location E by one and place the result back in E. Compare the result with zero,
* and skip the next instruction in sequence if the condition specified by M is satisfied. If location E originally
* contained -2^35 , set the Overflow and Carry 0 flags; if any other nonzero number, set Carry 0 and Carry 1.
* If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOSLE = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) <= 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSOSA(0o374000): Subtract One from Memory and Skip Always
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-46:
*
* Decrement the contents of location E by one and place the result back in E. Compare the result with zero,
* and skip the next instruction in sequence if the condition specified by M is satisfied. If location E originally
* contained -2^35 , set the Overflow and Carry 0 flags; if any other nonzero number, set Carry 0 and Carry 1.
* If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOSA = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), PDP10.WORD_MASK));
this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSOSGE(0o375000): Subtract One from Memory and Skip if Greater than or Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-46:
*
* Decrement the contents of location E by one and place the result back in E. Compare the result with zero,
* and skip the next instruction in sequence if the condition specified by M is satisfied. If location E originally
* contained -2^35 , set the Overflow and Carry 0 flags; if any other nonzero number, set Carry 0 and Carry 1.
* If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOSGE = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) >= 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSOSN(0o376000): Subtract One from Memory and Skip if Not Equal to Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-46:
*
* Decrement the contents of location E by one and place the result back in E. Compare the result with zero,
* and skip the next instruction in sequence if the condition specified by M is satisfied. If location E originally
* contained -2^35 , set the Overflow and Carry 0 flags; if any other nonzero number, set Carry 0 and Carry 1.
* If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOSN = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) != 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSOSG(0o377000): Subtract One from Memory and Skip if Greater than Zero
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-46:
*
* Decrement the contents of location E by one and place the result back in E. Compare the result with zero,
* and skip the next instruction in sequence if the condition specified by M is satisfied. If location E originally
* contained -2^35 , set the Overflow and Carry 0 flags; if any other nonzero number, set Carry 0 and Carry 1.
* If A is nonzero also place the result in AC.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSOSG = function(op, ac)
{
var dst = this.writeWord(this.regEA, PDP10.doADD.call(this, this.readWord(this.regEA), PDP10.WORD_MASK));
if (PDP10.SIGN(dst) > 0) this.setPC(this.regPC + 1);
if (ac) this.writeWord(ac, dst);
};
/**
* opSETZ(0o400000): Set to Zeros
* opSETZI(0o401000): Set to Zeros Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-18:
*
* Change the contents of the destination specified by M to all 0s.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETZ = function(op, ac)
{
this.writeWord(ac, 0);
};
/**
* opSETZM(0o402000): Set to Zeros Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-18:
*
* Change the contents of the destination specified by M to all 0s.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETZM = function(op, ac)
{
this.writeWord(this.regEA, 0);
};
/**
* opSETZB(0o403000): Set to Zeros Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-18:
*
* Change the contents of the destination specified by M to all 0s.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETZB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, 0));
};
/**
* opAND(0o404000): And with AC
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([AC]) to the AND function of the specified operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opAND = function(op, ac)
{
this.writeWord(ac, PDP10.AND(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opANDI(0o405000): And with AC Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([AC]) to the AND function of the specified operand (E) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDI = function(op, ac)
{
this.writeWord(ac, PDP10.AND(this.readWord(ac), this.regEA));
};
/**
* opANDM(0o406000): And with AC to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([E]) to the AND function of the specified operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.AND(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opANDB(0o407000): And with AC to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the AND function of the specified operand ([E])
* and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.AND(this.readWord(ac), this.readWord(this.regEA))));
};
/**
* opANDCA(0o410000): And with Complement of AC
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([AC]) to the AND function of the specified operand ([E])
* and the complement of [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCA = function(op, ac)
{
this.writeWord(ac, PDP10.AND(PDP10.WORD_MASK - this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opANDCAI(0o411000): And with Complement of AC Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([AC]) to the AND function of the specified operand (E)
* and the complement of [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCAI = function(op, ac)
{
this.writeWord(ac, PDP10.AND(PDP10.WORD_MASK - this.readWord(ac), this.regEA));
};
/**
* opANDCAM(0o412000): And with Complement of AC to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([E]) to the AND function of the specified operand ([E])
* and the complement of [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCAM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.AND(PDP10.WORD_MASK - this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opANDCAB(0o413000): And with Complement of AC to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the AND function of the specified operand ([E])
* and the complement of [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCAB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.AND(PDP10.WORD_MASK - this.readWord(ac), this.readWord(this.regEA))));
};
/**
* opANDCM(0o420000): And Complement of Memory with AC
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([AC]) to the AND function of the complement of the specified
* operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCM = function(op, ac)
{
this.writeWord(ac, PDP10.AND(this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA)));
};
/**
* opANDCMI(0o421000): And Complement of Memory Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([AC]) to the AND function of the complement of the specified
* operand (E) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCMI = function(op, ac)
{
this.writeWord(ac, PDP10.AND(this.readWord(ac), PDP10.WORD_MASK - this.regEA));
};
/**
* opANDCMM(0o422000): And Complement of Memory to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([E]) to the AND function of the complement of the specified
* operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCMM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.AND(this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA)));
};
/**
* opANDCMB(0o423000): And Complement of Memory to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-20:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the AND function of the complement of the specified
* operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCMB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.AND(this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA))));
};
/**
* opXOR(0o430000): Exclusive Or with AC
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([AC]) to the exclusive OR function of the specified
* operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opXOR = function(op, ac)
{
this.writeWord(ac, PDP10.XOR(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opXORI(0o431000): Exclusive Or Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([AC]) to the exclusive OR function of the specified
* operand (E) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opXORI = function(op, ac)
{
this.writeWord(ac, PDP10.XOR(this.readWord(ac), this.regEA));
};
/**
* opXORM(0o432000): Exclusive Or to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([E]) to the exclusive OR function of the specified
* operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opXORM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.XOR(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opXORB(0o433000): Exclusive Or to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the exclusive OR function of the specified
* operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opXORB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.XOR(this.readWord(ac), this.readWord(this.regEA))));
};
/**
* opIOR(0o434000): Inclusive Or with AC
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([AC]) to the inclusive OR function of the specified
* operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIOR = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opIORI(0o435000): Inclusive Or with AC Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([AC]) to the inclusive OR function of the specified
* operand (E) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIORI = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(this.readWord(ac), this.regEA));
};
/**
* opIORM(0o436000): Inclusive Or with AC to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([E]) to the inclusive OR function of the specified
* operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIORM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.IOR(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opIORB(0o437000): Inclusive Or with AC to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the inclusive OR function of the specified
* operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opIORB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.IOR(this.readWord(ac), this.readWord(this.regEA))));
};
/**
* opANDCB(0o440000): And Complements of Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([AC]) to the AND function of the complements of both
* the specified operand ([E]) and [AC]. The result is the NOR function of the operands.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCB = function(op, ac)
{
this.writeWord(ac, PDP10.AND(PDP10.WORD_MASK - this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA)));
};
/**
* opANDCBI(0o441000): And Complements of Both Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([AC]) to the AND function of the complements of both
* the specified operand (E) and [AC]. The result is the NOR function of the operands.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCBI = function(op, ac)
{
this.writeWord(ac, PDP10.AND(PDP10.WORD_MASK - this.readWord(ac), PDP10.WORD_MASK - this.regEA));
};
/**
* opANDCBM(0o442000): And Complements of Both to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([E]) to the AND function of the complements of both
* the specified operand ([E]) and [AC]. The result is the NOR function of the operands.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCBM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.AND(PDP10.WORD_MASK - this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA)));
};
/**
* opANDCBB(0o443000): And Complements of Both to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the AND function of the complements of both
* the specified operand ([E]) and [AC]. The result is the NOR function of the operands.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opANDCBB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.AND(PDP10.WORD_MASK - this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA))));
};
/**
* opEQV(0o444000): Equivalence with AC
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-23:
*
* Change the contents of the destination specified by M ([AC]) to the complement of the exclusive OR function of the
* specified operand ([E]) and [AC] (the result has 1s wherever the corresponding bits of the operands are the same).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opEQV = function(op, ac)
{
this.writeWord(ac, PDP10.EQV(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opEQVI(0o445000): Equivalence Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-23:
*
* Change the contents of the destination specified by M ([AC]) to the complement of the exclusive OR function of the
* specified operand (E) and [AC] (the result has 1s wherever the corresponding bits of the operands are the same).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opEQVI = function(op, ac)
{
this.writeWord(ac, PDP10.EQV(this.readWord(ac), this.regEA));
};
/**
* opEQVM(0o446000): Equivalence to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-23:
*
* Change the contents of the destination specified by M ([E]) to the complement of the exclusive OR function of the
* specified operand ([E]) and [AC] (the result has 1s wherever the corresponding bits of the operands are the same).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opEQVM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.EQV(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opEQVB(0o447000): Equivalence to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-23:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the complement of the exclusive OR function of the
* specified operand ([E]) and [AC] (the result has 1s wherever the corresponding bits of the operands are the same).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opEQVB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.EQV(this.readWord(ac), this.readWord(this.regEA))));
};
/**
* opSETCA(0o450000): Set to Complement of AC
* opSETCAI(0o451000): Set to Complement of AC Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-19:
*
* Change the contents of the destination specified by M ([AC]) to the complement of [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETCA = function(op, ac)
{
this.writeWord(ac, PDP10.WORD_MASK - this.readWord(ac));
};
/**
* opSETCAM(0o452000): Set to Complement of AC Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-19:
*
* Change the contents of the destination specified by M ([E]) to the complement of [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETCAM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.WORD_MASK - this.readWord(ac));
};
/**
* opSETCAB(0o453000): Set to Complement of AC Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-19:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the complement of [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETCAB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.WORD_MASK - this.readWord(ac)));
};
/**
* opORCA(0o454000): Inclusive Or with Complement of AC
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([AC]) to the inclusive OR function of the specified
* operand ([E]) and the complement of [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCA = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(PDP10.WORD_MASK - this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opORCAI(0o455000): Inclusive Or with Complement of AC Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([AC]) to the inclusive OR function of the specified
* operand (E) and the complement of [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCAI = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(PDP10.WORD_MASK - this.readWord(ac), this.regEA));
};
/**
* opORCAM(0o456000): Inclusive Or with Complement of AC to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([E]) to the inclusive OR function of the specified
* operand ([E]) and the complement of [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCAM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.IOR(PDP10.WORD_MASK - this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opORCAB(0o457000): Inclusive Or with Complement of AC to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-21:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the inclusive OR function of the specified
* operand ([E]) and the complement of [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCAB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.IOR(PDP10.WORD_MASK - this.readWord(ac), this.readWord(this.regEA))));
};
/**
* opSETCM(0o460000): Set to Complement of Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-19:
*
* Change the contents of the destination specified by M ([AC]) to the complement of the specified source
* operand ([E]).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETCM = function(op, ac)
{
this.writeWord(ac, PDP10.WORD_MASK - this.readWord(this.regEA));
};
/**
* opSETCMI(0o461000): Set to Complement of Memory Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-19:
*
* Change the contents of the destination specified by M ([AC]) to the complement of the specified source
* operand (E).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETCMI = function(op, ac)
{
this.writeWord(ac, PDP10.WORD_MASK - this.regEA);
};
/**
* opSETCMM(0o462000): Set to Complement of Memory Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-19:
*
* Change the contents of the destination specified by M ([E]) to the complement of the specified source
* operand ([E]).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETCMM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.WORD_MASK - this.readWord(this.regEA));
};
/**
* opSETCMB(0o463000): Set to Complement of Memory Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-19:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the complement of the specified source
* operand ([E]).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETCMB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.WORD_MASK - this.readWord(this.regEA)));
};
/**
* opORCM(0o464000): Inclusive Or Complement of Memory with AC
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([AC]) to the inclusive OR function of the complement of the
* specified operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCM = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA)));
};
/**
* opORCMI(0o465000): Inclusive Or Complement of Memory Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([AC]) to the inclusive OR function of the complement of the
* specified operand (E) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCMI = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(this.readWord(ac), PDP10.WORD_MASK - this.regEA));
};
/**
* opORCMM(0o466000): Inclusive Or Complement of Memory to Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([E]) to the inclusive OR function of the complement of the
* specified operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCMM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.IOR(this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA)));
};
/**
* opORCMB(0o467000): Inclusive Or Complement of Memory to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the inclusive OR function of the complement of the
* specified operand ([E]) and [AC].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCMB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.IOR(this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA))));
};
/**
* opORCB(0o470000): Inclusive Or Complements of Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([AC]) to the inclusive OR function of the complements of both
* the specified operand ([E]) and [AC]. The result is the NAND function of the operands.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCB = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(PDP10.WORD_MASK - this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA)));
};
/**
* opORCBI(0o471000): Inclusive Or Complements of Both Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([AC]) to the inclusive OR function of the complements of both
* the specified operand (E) and [AC]. The result is the NAND function of the operands.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCBI = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(PDP10.WORD_MASK - this.readWord(ac), PDP10.WORD_MASK - this.regEA));
};
/**
* opORCBM(0o472000): Inclusive Or Complements of Both To Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([E]) to the inclusive OR function of the complements of both
* the specified operand ([E]) and [AC]. The result is the NAND function of the operands.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCBM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.IOR(PDP10.WORD_MASK - this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA)));
};
/**
* opORCBB(0o473000): Inclusive Or Complements of Both to Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-22:
*
* Change the contents of the destination specified by M ([E] and [AC]) to the inclusive OR function of the complements of both
* the specified operand ([E]) and [AC]. The result is the NAND function of the operands.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opORCBB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.IOR(PDP10.WORD_MASK - this.readWord(ac), PDP10.WORD_MASK - this.readWord(this.regEA))));
};
/**
* opSETO(0o474000): Set to Ones
* opSETOI(0o475000): Set to Ones Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-18:
*
* Change the contents of the destination specified by M (AC) to all 1s.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETO = function(op, ac)
{
this.writeWord(ac, PDP10.WORD_MASK);
};
/**
* opSETOM(0o476000): Set to Ones Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-18:
*
* Change the contents of the destination specified by M ([E]) to all 1s.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETOM = function(op, ac)
{
this.writeWord(this.regEA, PDP10.WORD_MASK);
};
/**
* opSETOB(0o477000): Set to Ones Both
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-18:
*
* Change the contents of the destination specified by M ([E] and [AC]) to all 1s.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opSETOB = function(op, ac)
{
this.writeWord(this.regEA, this.writeWord(ac, PDP10.WORD_MASK));
};
/**
* opHLL(0o5N0000): Half Word Left to Left
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-3:
*
* Move the left half of the source word specified by M to the left half of the specified destination.
* The source and the destination right half are unaffected; the original contents of the destination are lost.
*
* For HLL, the source is [E] and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHLL = function(op, ac)
{
var src = this.readWord(this.regEA);
var dst = this.readWord(ac);
this.writeWord(ac, PDP10.GETHR(op, dst, src) + (src - (src & PDP10.HALF_MASK)));
};
/**
* opHLLI(0o5N1000): Half Word Left to Left, Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-3:
*
* Move the left half of the source word specified by M to the left half of the specified destination.
* The source and the destination right half are unaffected; the original contents of the destination are lost.
*
* SIDEBAR: HLLI merely clears AC left.
*
* For HLLI, the source is 0,E and the destination is [A]. But since this is a left-half-only operation, src is
* effectively 0.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHLLI = function(op, ac)
{
var dst = this.readWord(ac);
this.writeWord(ac, PDP10.GETHR(op, dst, 0));
};
/**
* opHLLM(0o5N2000): Half Word Left to Left, Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-3:
*
* Move the left half of the source word specified by M to the left half of the specified destination.
* The source and the destination right half are unaffected; the original contents of the destination are lost.
*
* For HLLM, the source is [A] and the destination is [E].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHLLM = function(op, ac)
{
var src = this.readWord(ac);
var dst = this.readWord(this.regEA);
this.writeWord(this.regEA, PDP10.GETHR(op, dst, src) + (src - (src & PDP10.HALF_MASK)));
};
/**
* opHLLS(0o5N3000): Half Word Left to Left, Self
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-3:
*
* Move the left half of the source word specified by M to the left half of the specified destination.
* The source and the destination right half are unaffected; the original contents of the destination are lost.
*
* SIDEBAR: If A is zero, HLLS is a no-op, otherwise it is equivalent to HLL.
*
* For HLLS, the source AND destination is [E] (and also [A] if A is non-zero).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHLLS = function(op, ac)
{
var dst = this.readWord(this.regEA);
dst = PDP10.SETHR(op, dst, dst);
this.writeWord(this.regEA, dst);
if (ac) this.writeWord(ac, dst);
};
/**
* opHRL(0o504000): Half Word Right to Left
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-4:
*
* Move the right half of the source word specified by M to the left half of the specified destination.
* The source and the destination right half are unaffected; the original contents of the destination left
* half are lost.
*
* For HRL, the source is [E] and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHRL = function(op, ac)
{
var src = (this.readWord(this.regEA) & PDP10.HALF_MASK) * PDP10.HALF_SHIFT;
var dst = this.readWord(ac);
this.writeWord(ac, PDP10.GETHR(op, dst, src) + src);
};
/**
* opHRLI(0o505000): Half Word Right to Left, Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-4:
*
* Move the right half of the source word specified by M to the left half of the specified destination.
* The source and the destination right half are unaffected; the original contents of the destination left
* half are lost.
*
* For HRLI, the source is 0,E and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHRLI = function(op, ac)
{
var src = this.regEA * PDP10.HALF_SHIFT;
var dst = this.readWord(ac);
this.writeWord(ac, PDP10.GETHR(op, dst, src) + src);
};
/**
* opHRLM(0o506000): Half Word Right to Left, Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-4:
*
* Move the right half of the source word specified by M to the left half of the specified destination.
* The source and the destination right half are unaffected; the original contents of the destination left
* half are lost.
*
* For HRLM, the source is [A] and the destination is [E].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHRLM = function(op, ac)
{
var src = (this.readWord(ac) & PDP10.HALF_MASK) * PDP10.HALF_SHIFT;
var dst = this.readWord(this.regEA);
this.writeWord(this.regEA, PDP10.GETHR(op, dst, src) + src);
};
/**
* opHRLS(0o507000): Half Word Right to Left, Self
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-4:
*
* Move the right half of the source word specified by M to the left half of the specified destination.
* The source and the destination right half are unaffected; the original contents of the destination left
* half are lost.
*
* For HRLS, the source AND destination is [E] (and also [A] if A is non-zero).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHRLS = function(op, ac)
{
var dst = this.readWord(this.regEA);
var src = (dst & PDP10.HALF_MASK) * PDP10.HALF_SHIFT;
dst = PDP10.GETHR(op, dst, src) + src;
this.writeWord(this.regEA, dst);
if (ac) this.writeWord(ac, dst);
};
/**
* opHRR(0o540000): Half Word Right to Right
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-6:
*
* Move the right half of the source word specified by M to the right half of the specified destination.
* The source and the destination left half are unaffected; the original contents of the destination right
* half are lost.
*
* For HRR, the source is [E] and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHRR = function(op, ac)
{
var src = this.readWord(this.regEA) & PDP10.HALF_MASK;
var dst = this.readWord(ac);
this.writeWord(ac, PDP10.GETHL(op, dst, src) + src);
};
/**
* opHRRI(0o541000): Half Word Right to Right, Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-6:
*
* Move the right half of the source word specified by M to the right half of the specified destination.
* The source and the destination left half are unaffected; the original contents of the destination right
* half are lost.
*
* For HRRI, the source is 0,E and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHRRI = function(op, ac)
{
var dst = this.readWord(ac);
this.writeWord(ac, PDP10.GETHL(op, dst, this.regEA) + this.regEA);
};
/**
* opHRRM(0o542000): Half Word Right to Right, Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-6:
*
* Move the right half of the source word specified by M to the right half of the specified destination.
* The source and the destination left half are unaffected; the original contents of the destination right
* half are lost.
*
* For HRRM, the source is [A] and the destination is [E].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHRRM = function(op, ac)
{
var src = this.readWord(ac) & PDP10.HALF_MASK;
var dst = this.readWord(this.regEA);
this.writeWord(this.regEA, PDP10.GETHL(op, dst, src) + src);
};
/**
* opHRRS(0o543000): Half Word Right to Right, Self
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-6:
*
* Move the right half of the source word specified by M to the right half of the specified destination.
* The source and the destination left half are unaffected; the original contents of the destination right
* half are lost.
*
* SIDEBAR: If A is zero, HRRS is a no-op; otherwise it is equivalent to HRR.
*
* For HRRS, the source AND destination is [E] (and also [A] if A is non-zero).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHRRS = function(op, ac)
{
var dst = this.readWord(this.regEA);
dst = PDP10.SETHL(op, dst, dst);
this.writeWord(this.regEA, dst);
if (ac) this.writeWord(ac, dst);
};
/**
* opHLR(0o544000): Half Word Left to Right
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-7:
*
* Move the left half of the source word specified by M to the right half of the specified destination.
* The source and the destination left half are unaffected; the original contents of the destination right half are lost.
*
* For HLR, the source is [E] and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHLR = function(op, ac)
{
var src = (this.readWord(this.regEA) / PDP10.HALF_SHIFT)|0;
var dst = this.readWord(ac);
this.writeWord(ac, PDP10.GETHL(op, dst, src) + src);
};
/**
* opHLRI(0o545000): Half Word Left to Right, Immediate
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-7:
*
* Move the left half of the source word specified by M to the right half of the specified destination.
* The source and the destination left half are unaffected; the original contents of the destination right half are lost.
*
* SIDEBAR: HLRI merely clears AC right.
*
* For HLRI, the source is 0,E and the destination is [A].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHLRI = function(op, ac)
{
var dst = this.readWord(ac);
this.writeWord(ac, PDP10.GETHL(op, dst, 0));
};
/**
* opHLRM(0o546000): Half Word Left to Right, Memory
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-7:
*
* Move the left half of the source word specified by M to the right half of the specified destination.
* The source and the destination left half are unaffected; the original contents of the destination right half are lost.
*
* For HLRM, the source is [A] and the destination is [E].
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHLRM = function(op, ac)
{
var src = (this.readWord(ac) / PDP10.HALF_SHIFT)|0;
var dst = this.readWord(this.regEA);
this.writeWord(this.regEA, PDP10.GETHL(op, dst, src) + src);
};
/**
* opHLRS(0o547000): Half Word Left to Right, Self
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-7:
*
* Move the left half of the source word specified by M to the right half of the specified destination.
* The source and the destination left half are unaffected; the original contents of the destination right half are lost.
*
* For HLRS, the source AND destination is [E] (and also [A] if A is non-zero).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opHLRS = function(op, ac)
{
var dst = this.readWord(this.regEA);
var src = (dst / PDP10.HALF_SHIFT)|0;
dst = PDP10.GETHL(op, dst, src) + src;
this.writeWord(this.regEA, dst);
if (ac) this.writeWord(ac, dst);
};
/**
* opTRNE(0o602000): Test Right, No Modification, and Skip if All Masked Bits Equal 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRNE = function(op, ac)
{
if (PDP10.AND(this.readWord(ac), this.regEA) == 0) this.setPC(this.regPC + 1);
};
/**
* opTLNE(0o603000): Test Left, No Modification, and Skip if All Masked Bits Equal 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLNE = function(op, ac)
{
if (PDP10.AND(this.readWord(ac), this.regEA * PDP10.HALF_SHIFT) == 0) this.setPC(this.regPC + 1);
};
/**
* opTRNA(0o604000): Test Right, No Modification, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRNA = function(op, ac)
{
this.setPC(this.regPC + 1);
};
/**
* opTLNA(0o605000): Test Left, No Modification, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLNA = function(op, ac)
{
this.setPC(this.regPC + 1);
};
/**
* opTRNN(0o606000): Test Right, No Modification, and Skip if Not All Masked Bits Equal 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRNN = function(op, ac)
{
if (PDP10.AND(this.readWord(ac), this.regEA) != 0) this.setPC(this.regPC + 1);
};
/**
* opTLNN(0o607000): Test Left, No Modification, and Skip if Not All Masked Bits Equal 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLNN = function(op, ac)
{
if (PDP10.AND(this.readWord(ac), this.regEA * PDP10.HALF_SHIFT) != 0) this.setPC(this.regPC + 1);
};
/**
* opTDNE(0o612000): Test Direct, No Modification, and Skip if All Masked Bits Equal 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDNE = function(op, ac)
{
if (PDP10.AND(this.readWord(ac), this.readWord(this.regEA)) == 0) this.setPC(this.regPC + 1);
};
/**
* opTSNE(0o613000): Test Swapped, No Modification, and Skip if All Masked Bits Equal 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSNE = function(op, ac)
{
if (PDP10.AND(this.readWord(ac), PDP10.SWAP(this.readWord(this.regEA))) == 0) this.setPC(this.regPC + 1);
};
/**
* opTDNA(0o614000): Test Direct, No Modification, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDNA = function(op, ac)
{
this.setPC(this.regPC + 1);
};
/**
* opTSNA(0o615000): Test Swapped, No Modification, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSNA = function(op, ac)
{
this.setPC(this.regPC + 1);
};
/**
* opTDNN(0o616000): Test Direct, No Modification, and Skip if Not All Masked Bits Equal 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDNN = function(op, ac)
{
if (PDP10.AND(this.readWord(ac), this.readWord(this.regEA)) != 0) this.setPC(this.regPC + 1);
};
/**
* opTSNN(0o617000): Test Swapped, No Modification, and Skip if Not All Masked Bits Equal 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSNN = function(op, ac)
{
if (PDP10.AND(this.readWord(ac), PDP10.SWAP(this.readWord(this.regEA))) != 0) this.setPC(this.regPC + 1);
};
/**
* opTRZ(0o620000): Test Right, Zeros, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRZ = function(op, ac)
{
this.writeWord(ac, PDP10.CLR(this.readWord(ac), this.regEA));
};
/**
* opTLZ(0o621000): Test Left, Zeros, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLZ = function(op, ac)
{
this.writeWord(ac, PDP10.CLR(this.readWord(ac), this.regEA * PDP10.HALF_SHIFT));
};
/**
* opTRZE(0o622000): Test Right, Zeros, and Skip if All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRZE = function(op, ac)
{
var dst = this.readWord(ac);
if (PDP10.AND(dst, this.regEA) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(dst, this.regEA));
};
/**
* opTLZE(0o623000): Test Left, Zeros, and Skip if All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLZE = function(op, ac)
{
var dst = this.readWord(ac);
var src = this.regEA * PDP10.HALF_SHIFT;
if (PDP10.AND(dst, src) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(dst, src));
};
/**
* opTRZA(0o624000): Test Right, Zeros, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRZA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(this.readWord(ac), this.regEA));
};
/**
* opTLZA(0o625000): Test Left, Zeros, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLZA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(this.readWord(ac), this.regEA * PDP10.HALF_SHIFT));
};
/**
* opTRZN(0o626000): Test Right, Zeros, and Skip if Not All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRZN = function(op, ac)
{
var dst = this.readWord(ac);
if (PDP10.AND(dst, this.regEA) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(dst, this.regEA));
};
/**
* opTLZN(0o627000): Test Left, Zeros, and Skip if Not All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLZN = function(op, ac)
{
var dst = this.readWord(ac);
var src = this.regEA * PDP10.HALF_SHIFT;
if (PDP10.AND(dst, src) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(dst, src));
};
/**
* opTDZ(0o630000): Test Direct, Zeros, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDZ = function(op, ac)
{
this.writeWord(ac, PDP10.CLR(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opTSZ(0o631000): Test Swapped, Zeros, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSZ = function(op, ac)
{
this.writeWord(ac, PDP10.CLR(this.readWord(ac), PDP10.SWAP(this.readWord(this.regEA))));
};
/**
* opTDZE(0o632000): Test Direct, Zeros, and Skip if All Masked Bits Equaled a
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDZE = function(op, ac)
{
var dst = this.readWord(ac);
var src = this.readWord(this.regEA);
if (PDP10.AND(dst, src) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(dst, src));
};
/**
* opTSZE(0o633000): Test Swapped, Zeros, and Skip if All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSZE = function(op, ac)
{
var dst = this.readWord(ac);
var src = PDP10.SWAP(this.readWord(this.regEA));
if (PDP10.AND(dst, src) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(dst, src));
};
/**
* opTDZA(0o634000): Test Direct, Zeros, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDZA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opTSZA(0o635000): Test Swapped, Zeros, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSZA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(this.readWord(ac), PDP10.SWAP(this.readWord(this.regEA))));
};
/**
* opTDZN(0o636000): Test Direct, Zeros, and Skip if Not All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDZN = function(op, ac)
{
var dst = this.readWord(ac);
var src = this.readWord(this.regEA);
if (PDP10.AND(dst, src) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(dst, src));
};
/**
* opTSZN(0o637000): Test Swapped, Zeros, and Skip if Not All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSZN = function(op, ac)
{
var dst = this.readWord(ac);
var src = PDP10.SWAP(this.readWord(this.regEA));
if (PDP10.AND(dst, src) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.CLR(dst, src));
};
/**
* opTRC(0o640000): Test Right, Complement, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRC = function(op, ac)
{
this.writeWord(ac, PDP10.XOR(this.readWord(ac), this.regEA));
};
/**
* opTLC(0o641000): Test Left, Complement, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLC = function(op, ac)
{
this.writeWord(ac, PDP10.XOR(this.readWord(ac), this.regEA * PDP10.HALF_SHIFT));
};
/**
* opTRCE(0o642000): Test Right, Complement, and Skip if All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRCE = function(op, ac)
{
var dst = this.readWord(ac);
if (PDP10.AND(dst, this.regEA) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(dst, this.regEA));
};
/**
* opTLCE(0o643000): Test Left, Complement, and Skip if All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLCE = function(op, ac)
{
var dst = this.readWord(ac);
var src = this.regEA * PDP10.HALF_SHIFT;
if (PDP10.AND(dst, src) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(dst, src));
};
/**
* opTRCA(0o644000): Test Right, Complement, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRCA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(this.readWord(ac), this.regEA));
};
/**
* opTLCA(0o645000): Test Left, Complement, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLCA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(this.readWord(ac), this.regEA * PDP10.HALF_SHIFT));
};
/**
* opTRCN(0o646000): Test Right, Complement, and Skip if Not All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRCN = function(op, ac)
{
var dst = this.readWord(ac);
if (PDP10.AND(dst, this.regEA) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(dst, this.regEA));
};
/**
* opTLCN(0o647000): Test Left, Complement, and Skip if Not All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLCN = function(op, ac)
{
var dst = this.readWord(ac);
var src = this.regEA * PDP10.HALF_SHIFT;
if (PDP10.AND(dst, src) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(dst, src));
};
/**
* opTDC(0o650000): Test Direct, Complement, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDC = function(op, ac)
{
this.writeWord(ac, PDP10.XOR(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opTSC(0o651000): Test Swapped, Complement, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSC = function(op, ac)
{
this.writeWord(ac, PDP10.XOR(this.readWord(ac), PDP10.SWAP(this.readWord(this.regEA))));
};
/**
* opTDCE(0o652000): Test Direct, Complement, and Skip if All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDCE = function(op, ac)
{
var dst = this.readWord(ac);
var src = this.readWord(this.regEA);
if (PDP10.AND(dst, src) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(dst, src));
};
/**
* opTSCE(0o653000): Test Swapped, Complement, and Skip if All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSCE = function(op, ac)
{
var dst = this.readWord(ac);
var src = PDP10.SWAP(this.readWord(this.regEA));
if (PDP10.AND(dst, src) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(dst, src));
};
/**
* opTDCA(0o654000): Test Direct, Complement, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDCA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opTSCA(0o655000): Test Swapped, Complement, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSCA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(this.readWord(ac), PDP10.SWAP(this.readWord(this.regEA))));
};
/**
* opTDCN(0o656000): Test Direct, Complement, and Skip if Not All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDCN = function(op, ac)
{
var dst = this.readWord(ac);
var src = this.readWord(this.regEA);
if (PDP10.AND(dst, src) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(dst, src));
};
/**
* opTSCN(0o657000): Test Swapped, Complement, and Skip if Not All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSCN = function(op, ac)
{
var dst = this.readWord(ac);
var src = PDP10.SWAP(this.readWord(this.regEA));
if (PDP10.AND(dst, src) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.XOR(dst, src));
};
/**
* opTRO(0o660000): Test Right, Ones, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRO = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(this.readWord(ac), this.regEA));
};
/**
* opTLO(0o661000): Test Left, Ones, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLO = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(this.readWord(ac), this.regEA * PDP10.HALF_SHIFT));
};
/**
* opTROE(0o662000): Test Right, Ones, and Skip if All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTROE = function(op, ac)
{
var dst = this.readWord(ac);
if (PDP10.AND(dst, this.regEA) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(dst, this.regEA));
};
/**
* opTLOE(0o663000): Test Left, Ones, and Skip if All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLOE = function(op, ac)
{
var dst = this.readWord(ac);
if (PDP10.AND(dst, this.regEA * PDP10.HALF_SHIFT) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(dst, this.regEA * PDP10.HALF_SHIFT));
};
/**
* opTROA(0o664000): Test Right, Ones, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTROA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(this.readWord(ac), this.regEA));
};
/**
* opTLOA(0o665000): Test Left, Ones, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLOA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(this.readWord(ac), this.regEA * PDP10.HALF_SHIFT));
};
/**
* opTRON(0o666000): Test Right, Ones, and Skip if Not All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTRON = function(op, ac)
{
var dst = this.readWord(ac);
if (PDP10.AND(dst, this.regEA) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(dst, this.regEA));
};
/**
* opTLON(0o667000): Test Left, Ones, and Skip if Not All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTLON = function(op, ac)
{
var dst = this.readWord(ac);
var src = this.regEA * PDP10.HALF_SHIFT;
if (PDP10.AND(dst, src) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(dst, src));
};
/**
* opTDO(0o670000): Test Direct, Ones, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDO = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opTSO(0o671000): Test Swapped, Ones, but Do Not Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSO = function(op, ac)
{
this.writeWord(ac, PDP10.IOR(this.readWord(ac), PDP10.SWAP(this.readWord(this.regEA))));
};
/**
* opTDOE(0o672000): Test Direct, Ones, and Skip if All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDOE = function(op, ac)
{
var dst = this.readWord(ac);
var src = this.readWord(this.regEA);
if (PDP10.AND(dst, src) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(dst, src));
};
/**
* opTSOE(0o673000): Test Swapped, Ones, and Skip if All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSOE = function(op, ac)
{
var dst = this.readWord(ac);
var src = PDP10.SWAP(this.readWord(this.regEA));
if (PDP10.AND(dst, src) == 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(dst, src));
};
/**
* opTDOA(0o674000): Test Direct, Ones, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDOA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(this.readWord(ac), this.readWord(this.regEA)));
};
/**
* opTSOA(0o675000): Test Swapped, Ones, but Always Skip
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSOA = function(op, ac)
{
this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(this.readWord(ac), PDP10.SWAP(this.readWord(this.regEA))));
};
/**
* opTDON(0o676000): Test Direct, Ones, and Skip if Not All Masked Bits Equaled 0
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTDON = function(op, ac)
{
var dst = this.readWord(ac);
var src = this.readWord(this.regEA);
if (PDP10.AND(dst, src) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(dst, src));
};
/**
* opTSON(0o677000): Test Swapped, Ones, and Skip if Not All Masked Bits Equaled a
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opTSON = function(op, ac)
{
var dst = this.readWord(ac);
var src = PDP10.SWAP(this.readWord(this.regEA));
if (PDP10.AND(dst, src) != 0) this.setPC(this.regPC + 1);
this.writeWord(ac, PDP10.IOR(dst, src));
};
/**
* opBLKI(0o700000)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} dev
*/
PDP10.opBLKI = function(op, dev)
{
this.opUndefined(op);
};
/**
* opDATAI(0o700040)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} dev
*/
PDP10.opDATAI = function(op, dev)
{
this.opUndefined(op);
};
/**
* opBLKO(0o700100)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} dev
*/
PDP10.opBLKO = function(op, dev)
{
this.opUndefined(op);
};
/**
* opDATAO(0o700140)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} dev
*/
PDP10.opDATAO = function(op, dev)
{
this.opUndefined(op);
};
/**
* opCONO(0o700200)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} dev
*/
PDP10.opCONO = function(op, dev)
{
switch(dev) {
case PDP10.DEVICES.APR:
this.writeFlags(this.readWord(this.regEA));
break;
default:
this.opUndefined(op);
break;
}
};
/**
* opCONI(0o700240)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} dev
*/
PDP10.opCONI = function(op, dev)
{
switch(dev) {
case PDP10.DEVICES.APR:
this.writeWord(this.regEA, this.readFlags());
break;
default:
this.opUndefined(op);
break;
}
};
/**
* opCONSZ(0o700300)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} dev
*/
PDP10.opCONSZ = function(op, dev)
{
this.opUndefined(op);
};
/**
* opCONSO(0o700340)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} dev
*/
PDP10.opCONSO = function(op, dev)
{
this.opUndefined(op);
};
/**
* opIO(0o700xxx-0o777xxx): Input-Output Instructions
*
* From the DEC PDP-10 System Reference Manual (May 1968), p. 2-68:
*
* The input-output instructions govern all transfers of data to and from the peripheral equipment, and also
* perform many operations within the processor. An instruction in the in-out class is designated by 111 in
* bits 0-2, ie its left octal digit is 7. Bits 3-9 address the device that is to respond to the instruction.
* The format thus allows for 128 codes, two of which, 000 and 004 respectively, address the processor and
* priority interrupt, and are used for the console and time share hardware as well. A chart in Appendix A
* lists all devices for which codes have been assigned, and gives their mnemonics and DEC option numbers.
*
* Bits 13-35 are the same as in all other instructions: they are the I, X, and Y parts, which are used to
* calculate an effective address, set of conditions, or mask to be used in the execution of the instruction.
* The remaining bits, 10-12, select one of the following eight 10 instructions.
*
* @this {CPUStatePDP10}
* @param {number} op
*/
PDP10.opIO = function(op)
{
PDP10.aOpIO_KA10[op & 7].call(this, op, (op >> 3) & 0o177);
};
/**
* opNOP(op, ac)
*
* Used for all "defined" operations that, in fact, do nothing (eg, SETA, SETAI, CAI, JUMP).
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opNOP = function(op, ac)
{
};
/**
* opNOPM(op, ac)
*
* Used for all "defined" operations that, in fact, do nothing (eg, SETMM, CAM) EXCEPT reference memory.
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} ac
*/
PDP10.opNOPM = function(op, ac)
{
};
/**
* opUndefined(op, ac)
*
* @this {CPUStatePDP10}
* @param {number} op
* @param {number} [ac]
*/
PDP10.opUndefined = function(op, ac)
{
this.println("undefined opcode: " + Str.toOct(op));
this.advancePC(-1);
this.stopCPU();
};
/**
* doABS(src)
*
* Returns the absolute value (ABS) of the 36-bit operand; used by the MOVM* (Move Magnitude) instructions.
*
* @this {CPUStatePDP10}
* @param {number} src (36-bit)
* @return {number} (absolute value of src)
*/
PDP10.doABS = function(src)
{
if (src > PDP10.INT_MASK) {
if (src != PDP10.INT_LIMIT) {
src = PDP10.TWO_POW36 - src;
} else {
this.regPS |= (PDP10.PSFLAG.AROV | PDP10.PSFLAG.CRY1);
}
}
return src;
};
/**
* doADD(dst, src)
*
* Performs the addition (ADD) of two signed 36-bit operands.
*
* @this {CPUStatePDP10}
* @param {number} dst (36-bit value)
* @param {number} src (36-bit value)
* @return {number} (dst + src)
*/
PDP10.doADD = function(dst, src)
{
/*
* Since, in our happy little world, 36-bit values are always unsigned, the
* only possible out-of-bounds value is a result >= WORD_LIMIT, which the mod cures.
*/
var res = (dst + src) % PDP10.WORD_LIMIT;
PDP10.setAddFlags.call(this, dst, src, res);
return res;
};
/**
* doDIV(src, dst, ext)
*
* Performs the division (DIV) of a 72-bit operand by a 36-bit operand.
*
* @this {CPUStatePDP10}
* @param {number} src (36-bit divisor)
* @param {number} dst (36-bit value)
* @param {number} [ext] (36-bit value extension)
* @return {number} (dst / src) (the remainder is stored in regEX); -1 if error (no division performed)
*/
PDP10.doDIV = function(src, dst, ext)
{
var fNegQ = false, fNegR = false;
/*
* Perform all the same up-front checks that the PDP-10 performs; if we do our job correctly, then it
* will be impossible for the actual division operation to overflow (the asserts below should never fire).
*/
if (ext === undefined) {
if (!src) {
this.regPS |= PDP10.PSFLAG.DCK | PDP10.PSFLAG.AROV;
return -1;
}
ext = (dst > PDP10.INT_MASK)? PDP10.WORD_MASK : 0;
} else {
var srcAbs = (src < PDP10.INT_LIMIT? src : PDP10.TWO_POW36 - src);
var extAbs = (ext < PDP10.INT_LIMIT? ext : PDP10.TWO_POW36 - ext - (dst? 1 : 0));
if (extAbs >= srcAbs) {
this.regPS |= PDP10.PSFLAG.DCK | PDP10.PSFLAG.AROV;
return -1;
}
}
/*
* We're done with the PDP-10's "dual sign" operands now; produce a (unified) signed 72-bit dividend.
*/
dst = PDP10.merge72.call(this, dst, ext);
ext = this.regEX;
/*
* Make all the inputs positive now, to keep the division simple. Fix up the results when we're done.
*/
if (src > PDP10.INT_MASK) {
src = PDP10.WORD_LIMIT - src;
fNegQ = !fNegQ;
}
if (ext > PDP10.INT_MASK) {
if (dst) {
ext = PDP10.WORD_MASK - ext;
dst = PDP10.WORD_LIMIT - dst;
}
else {
if (ext) ext = PDP10.WORD_LIMIT - ext;
}
fNegR = true; fNegQ = !fNegQ;
}
/*
* Initialize the four double-length 72-bit values we need for the division process.
*
* The process involves shifting the divisor left 1 bit (ie, doubling it) until it equals
* or exceeds the dividend, and then repeatedly subtracting the divisor from the dividend and
* shifting the divisor right 1 bit until the divisor is "exhausted" (no bits left), with an
* "early out" if the dividend gets "exhausted" first.
*
* Note that each element of these double arrays is a 36-bit value, so it's rarely a good idea
* to use bitwise operators on them, because those would operate on only the low 32 bits.
* Stick with the double worker functions I've created, and trust your JavaScript engine to
* inline/optimize the code.
*/
PDP10.INITD(this.regRes, 0, 0);
PDP10.INITD(this.regPow, 1, 0);
PDP10.INITD(this.regDiv, src, 0);
PDP10.INITD(this.regRem, dst, ext);
while (PDP10.CMPD(this.regRem, this.regDiv) > 0) {
PDP10.ADDD(this.regDiv, this.regDiv);
PDP10.ADDD(this.regPow, this.regPow);
}
do {
if (PDP10.CMPD(this.regRem, this.regDiv) >= 0) {
PDP10.SUBD(this.regRem, this.regDiv);
PDP10.ADDD(this.regRes, this.regPow);
if (PDP10.ZEROD(this.regRem)) break;
}
PDP10.SHRD(this.regDiv);
PDP10.SHRD(this.regPow);
} while (!PDP10.ZEROD(this.regPow));
dst = this.regRes[0];
this.regEX = this.regRem[0];
if (fNegQ && dst) {
dst = PDP10.WORD_LIMIT - dst;
}
if (fNegR && this.regEX) {
this.regEX = PDP10.WORD_LIMIT - this.regEX;
}
return dst;
};
/**
* doMUL(dst, src, fTruncate, fExternal)
*
* Performs the multiplication (MUL) of two signed 36-bit operands.
*
* To support 72-bit results, we perform the multiplication process as you would "by hand",
* treating the operands to be multiplied as two 2-digit numbers, where each "digit" is an 18-bit
* number (base 2^18). Each individual multiplication of these 18-bit "digits" will produce
* a result within 2^36, well within JavaScript integer accuracy.
*
* PDP-10 Diagnostic Notes
* -----------------------
*
* The "DAKAK" diagnostic contains the following code:
*
* 036174: 200240 043643 MOVE 5,43643 ; [43643] = 400000000000
* 036175: 200300 043603 MOVE 6,43603 ; [43603] = 777777777777
* 036176: 200140 043604 MOVE 3,43604 ; [43604] = 000000000001
* 036177: 224240 000003 MUL 5,3 ; Multiply 400000000000 by 000000000001
* 036200: 312240 043604 CAME 5,43604 ; high order result in AC should be: 000000000001
* 036201: 003240 033721 UUO 5,33721 ;
* 036202: 312300 043602 CAME 6,43602 ; low order result in AC+1 should be: 000000000000
*
* The "natural" result is:
*
* 05=777777777777 06=400000000000
*
* And SIMH seems to agree. So why does the DEC diagnostic expect:
*
* 05=000000000001 06=000000000000
*
* The answer can be found in the June 1982 "DECSYSTEM-10 and DECSYSTEM-20 Processor Reference Manual",
* in the description of the MUL instruction:
*
* CAUTION: In the KA10, an AC operand of 2^35 is treated as though it were +2^35, producing the
* incorrect sign in the product.
*
* This behavior is now simulated below for MODEL_KA10, at least to the extent that the diagnostic is happy.
*
* @this {CPUStatePDP10}
* @param {number} dst (36-bit value)
* @param {number} src (36-bit value)
* @param {boolean} [fTruncate] (true to truncate the result to 36 bits; used by IMUL instructions)
* @param {boolean} [fExternal] (true if external caller; avoids modifying the CPU state)
* @return {number} (dst * src) (the high 36 bits of the result; the low 36 bits are stored in regEX)
*/
PDP10.doMUL = function(dst, src, fTruncate, fExternal)
{
var n1 = dst, n2 = src;
var fNeg = false, res, ext;
/*
* If either input is in the negative range, record the sign and make it positive;
* we'll negate the result afterward if necessary.
*/
if (n1 > PDP10.INT_MASK) {
if (fExternal || this.model != PDP10.MODEL_KA10 || n1 != PDP10.INT_LIMIT) {
n1 = PDP10.WORD_LIMIT - n1;
fNeg = !fNeg;
}
}
if (n2 > PDP10.INT_MASK) {
n2 = PDP10.WORD_LIMIT - n2;
fNeg = !fNeg;
}
if (n1 < PDP10.HALF_SHIFT && n2 < PDP10.HALF_SHIFT) {
res = n1 * n2;
ext = 0;
}
else {
var n1d1 = (n1 & PDP10.HALF_MASK);
var n1d2 = Math.trunc(n1 / PDP10.HALF_SHIFT);
var n2d1 = (n2 & PDP10.HALF_MASK);
var n2d2 = Math.trunc(n2 / PDP10.HALF_SHIFT);
var m1d1 = n1d1 * n2d1;
var m1d2 = (n1d2 * n2d1) + Math.trunc(m1d1 / PDP10.HALF_SHIFT);
ext = Math.trunc(m1d2 / PDP10.HALF_SHIFT);
m1d2 = (m1d2 & PDP10.HALF_MASK) + (n1d1 * n2d2);
res = ((m1d2 * PDP10.HALF_SHIFT) + (m1d1 & PDP10.HALF_MASK)) % PDP10.WORD_LIMIT;
ext += Math.trunc(m1d2 / PDP10.HALF_SHIFT) + (n1d2 * n2d2);
}
if (fNeg) {
if (res) {
ext = PDP10.WORD_MASK - ext;
res = PDP10.WORD_LIMIT - res;
}
else {
if (ext) ext = PDP10.WORD_LIMIT - ext;
}
}
ext = PDP10.split72.call(this, res, ext, fExternal);
if (fTruncate) {
/*
* Special code for IMUL. I originally tried to avoid calling split72() in this case, but the PDP-10
* still appears to be splitting the result of the multiplication into separately signed 36-bit values,
* so the simplest solution is to call split72() in all cases.
*/
res = this.regEX;
if (!fExternal) {
/*
* Regarding IMUL overflows, the original spec says:
*
* Set Overflow if the product is >= 2^35 or < -2^35 (ie, if the high order word of the double
* length product is not null); the high order word is lost.
*
* However, when the "DAKAL" diagnostic performs a sequence like this:
*
* 00=000000000000 01=000000000000 02=000000000000 03=400000036755
* 04=400000000000 05=000000000003 06=400000000000 07=000000000004
* 10=000000000000 11=000000000011 12=777777400000 13=777777777776
* 14=000000200000 15=400000037134 16=000000000016 17=000000000000
* PC=037145 RA=00400000 EA=400000 PS=000000 OV=0 C0=0 C1=0 ND=0 PD=0
* 037145: 220600 000013 IMUL 14,13 ;cycles=1
*
* it sets 14=777777400000 and leaves overflow clear; since the 'high order word" would be 777777777777,
* not null, I think the spec over-simplifies. So our check is more exhaustive: it verifies that ext is
* nothing more than an extension of the sign bit of res (ie, 0 if res is positive, -1 if res is negative).
* Any other combination of values implies an overflow.
*/
if ((ext || res > PDP10.INT_MASK) && (ext != PDP10.WORD_MASK || res <= PDP10.INT_MASK)) {
this.regPS |= PDP10.PSFLAG.AROV;
}
}
ext = res;
}
return ext;
};
/**
* doNEG(src)
*
* Returns the negation (NEG) of the 36-bit operand; used by the MOVN* (Move Negative) instructions.
*
* @this {CPUStatePDP10}
* @param {number} src (36-bit)
* @return {number} (src negated, but as an unsigned 36-bit result)
*/
PDP10.doNEG = function(src)
{
if (!src) {
this.regPS |= (PDP10.PSFLAG.CRY0 | PDP10.PSFLAG.CRY1);
}
else {
/*
* In the non-zero case, it's always safe to subtract src from TWO_POW36, but since we have to check for
* the INT_LIMIT case anyway, and since subtraction in that case doesn't alter src, we skip the subtraction.
*/
if (src == PDP10.INT_LIMIT) {
this.regPS |= (PDP10.PSFLAG.AROV | PDP10.PSFLAG.CRY1);
} else {
src = PDP10.TWO_POW36 - src;
}
}
return src;
};
/**
* doSUB(dst, src)
*
* Performs the subtraction (SUB) of two signed 36-bit operands.
*
* @this {CPUStatePDP10}
* @param {number} dst (36-bit value)
* @param {number} src (36-bit value)
* @return {number} (dst - src)
*/
PDP10.doSUB = function(dst, src)
{
/*
* Since, in our happy little world, 36-bit values are always unsigned, the
* only possible out-of-bounds value is a result < 0, which adding WORD_LIMIT cures.
*/
var res = (dst - src);
if (res < 0) res += PDP10.WORD_LIMIT;
/*
* We can leverage setAddFlags() by treating the subtraction as addition;
* since res = dst - src, it is also true that dst = res + src.
*/
PDP10.setAddFlags.call(this, res, src, dst);
return res;
};
/**
* merge72(dst, ext)
*
* Returns a unified 72-bit result from two independently-signed 36-bit values.
*
* @this {CPUStatePDP10}
* @param {number} dst (36-bit value)
* @param {number} ext (36-bit value)
* @return {number} (returns the lower 36 bits; the upper 36 bits are stored in regEX)
*/
PDP10.merge72 = function(dst, ext)
{
var sign = (ext - (ext % PDP10.INT_LIMIT));
/*
* For 72-bit inputs, the PDP-10 doesn't care whether or not the low word's sign
* matches the high word's sign. The high word's sign bit is the controlling bit.
*
*
*
* Compute value without the sign bit and add the low bit of extended in its place.
*/
dst = (dst % PDP10.INT_LIMIT) + ((ext & 1) * PDP10.INT_LIMIT);
this.regEX = sign + Math.trunc(ext / 2);
return dst;
};
/**
* split72(res, ext, fExternal)
*
* Returns two 36-bit values with matching sign bits from a unified 72-bit result.
*
* @this {CPUStatePDP10}
* @param {number} res (36-bit value)
* @param {number} ext (36-bit value)
* @param {boolean} [fExternal] (true if external caller; avoids modifying the CPU state)
* @return {number} (returns the upper 36 bits; the lower 36 bits are stored in regEX)
*/
PDP10.split72 = function(res, ext, fExternal)
{
/*
* We just produced a signed 72-bit result, whereas the PDP-10 stores 72-bit arithmetic values as two
* signed 36-bit results with matching signs. Since that's effectively only 70 bits of magnitude (with
* two sign bits), we lose one bit of magnitude.
*
* The conversion requires shifting ext left one bit so that we can move the high bit of res into the
* low bit of ext, and then set the sign bit of res to match the sign bit of ext.
*/
var sign = ext - (ext % PDP10.INT_LIMIT);
ext = ((ext * 2) % PDP10.WORD_LIMIT) + Math.trunc(res / PDP10.INT_LIMIT);
res = sign + (res % PDP10.INT_LIMIT);
var signNew = ext - (ext % PDP10.INT_LIMIT);
if (signNew != sign) {
/*
* I used to restore ext's original sign (ext = sign + (ext - signNew)), but the PDP-10's defined
* behavior for multiplication overflow (ie, whenever both operands are 0o400000000000) is to set both
* res and ext to 0o400000000000.
*
* To quote the original spec for the MUL instruction:
*
* If both operands are -2^35 set Overflow; the double length result stored is -2^70.
*/
res = ext;
}
if (fExternal) return res;
if (res == PDP10.INT_LIMIT && ext == PDP10.INT_LIMIT) {
this.regPS |= PDP10.PSFLAG.AROV;
}
this.regEX = res;
return ext;
};
/**
* setAddFlags(dst, src, res)
*
* @this {CPUStatePDP10}
* @param {number} dst (36-bit value)
* @param {number} src (36-bit value)
* @param {number} res (36-bit value)
*/
PDP10.setAddFlags = function(dst, src, res)
{
/*
* Isolate the top two bits of dst, src, and res by "shifting" them into bits 0 and 1 of the
* following variables. Note that shifting with division only works when the values are unsigned
* (which they MUST be).
*/
var dst01 = Math.trunc(dst / PDP10.TWO_POW34);
var src01 = Math.trunc(src / PDP10.TWO_POW34);
var res01 = Math.trunc(res / PDP10.TWO_POW34);
/*
* Transform bits 0 and 1 into carry flags, based on the following truth table:
*
* D S R C Carry?
* - - - - ------
* 0 0 0 0 no
* 0 0 1 0 no (there must have been a carry out of the preceding bit, but it was "absorbed")
* 0 1 0 1 yes (there must have been a carry out of the preceding bit, but it was NOT "absorbed")
* 0 1 1 0 no
* 1 0 0 1 yes (same as the preceding "yes" case)
* 1 0 1 0 no
* 1 1 0 1 yes (since the addition of two ones must always produce a carry)
* 1 1 1 1 yes (since the addition of two ones must always produce a carry)
*/
var bitsCarry = (dst01 ^ ((dst01 ^ src01) & (src01 ^ res01)));
var fCarry0 = bitsCarry & 0b10;
var fCarry1 = bitsCarry & 0b01;
/*
* Similarly, we transform bit 1 into an overflow flag, based on the following truth table;
* note that X is (D ^ R) and Y is (S ^ R):
*
* D S R X Y O Overflow?
* - - - - - - ---------
* 0 0 0 0 0 0 no
* 0 0 1 1 1 1 yes (adding two positive values yielded a negative value)
* 0 1 0 0 1 0 no
* 0 1 1 1 0 0 no
* 1 0 0 1 0 0 no
* 1 0 1 0 1 0 no
* 1 1 0 1 1 1 yes (adding two negative values yielded a positive value)
* 1 1 1 0 0 0 no
*/
var fOverflow = ((dst01 ^ res01) & (src01 ^ res01)) & 0b10;
this.regPS |= (fCarry0? PDP10.PSFLAG.CRY0 : 0) | (fCarry1? PDP10.PSFLAG.CRY1 : 0) | (fOverflow? PDP10.PSFLAG.AROV : 0);
};
/**
* AND(dst, src)
*
* Performs the logical "and" (AND) of two 36-bit operands.
*
* @param {number} dst (36-bit value)
* @param {number} src (36-bit value)
* @return {number} (dst & src)
*/
PDP10.AND = function(dst, src)
{
/*
* Since dst and src are 36-bit values, we must AND the low 32 bits separately from the higher bits,
* and then combine them with addition. Since all bits above 36 will be zero, and since 0 AND 0 is 0,
* no special masking for the higher bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*
* Finally, all 36-bit data within a PDP-10 machine should ALWAYS be unsigned, which we now assert,
* because the divisions below would not yield correct results with negative inputs.
*/
return ((((dst / PDP10.TWO_POW32)|0) & ((src / PDP10.TWO_POW32)|0)) * PDP10.TWO_POW32) + ((dst & src) >>> 0);
};
/**
* CLR(dst, src)
*
* Performs the logical "and" (AND) of a 36-bit operand and its complement.
*
* @param {number} dst (36-bit value)
* @param {number} src (36-bit value)
* @return {number} (dst & ~src)
*/
PDP10.CLR = function(dst, src)
{
return PDP10.AND(dst, PDP10.NOT(src));
};
/**
* CMP(dst, src)
*
* Performs the SIGNED comparison (CMP) of two 36-bit operands.
*
* @param {number} dst (36-bit value)
* @param {number} src (36-bit value)
* @return {number} (dst - src)
*/
PDP10.CMP = function(dst, src)
{
return (dst < PDP10.INT_LIMIT? dst : dst - PDP10.WORD_LIMIT) - (src < PDP10.INT_LIMIT? src : src - PDP10.WORD_LIMIT);
};
/**
* EQV(dst, src)
*
* Performs the logical "equivalence" (EQV) of two 36-bit operands (ie, NOT XOR)
*
* @param {number} dst (36-bit value)
* @param {number} src (36-bit value)
* @return {number} (~(dst ^ src))
*/
PDP10.EQV = function(dst, src)
{
/*
* Since dst and src are 36-bit values, we must EQV the low 32 bits separately from the higher bits,
* and then combine them with addition. Since all bits above 36 will be zero, and since 0 EQV 0 is 1,
* we must mask the higher 4 bits with 0o17.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*
* Finally, all 36-bit data within a PDP-10 machine should ALWAYS be unsigned, which we now assert,
* because the divisions below would not yield correct results with negative inputs.
*/
return ((~(((dst / PDP10.TWO_POW32)|0) ^ ((src / PDP10.TWO_POW32)|0)) & 0o17) * PDP10.TWO_POW32) + (~(dst ^ src) >>> 0);
};
/**
* IOR(dst, src)
*
* Performs the logical "inclusive-or" (OR) of two 36-bit operands.
*
* @param {number} dst (36-bit value)
* @param {number} src (36-bit value)
* @return {number} (dst | src)
*/
PDP10.IOR = function(dst, src)
{
/*
* Since dst and src are 36-bit values, we must OR the low 32 bits separately from the higher bits,
* and then combine them with addition. Since all bits above 36 will be zero, and since 0 OR 0 is 0,
* no special masking for the higher bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*
* Finally, all 36-bit data within a PDP-10 machine should ALWAYS be unsigned, which we now assert,
* because the divisions below would not yield correct results with negative inputs.
*/
return ((((dst / PDP10.TWO_POW32)|0) | ((src / PDP10.TWO_POW32)|0)) * PDP10.TWO_POW32) + ((dst | src) >>> 0);
};
/**
* NOT(src)
*
* Performs the one's complement (NOT) of a 36-bit operand.
*
* @param {number} src (36-bit value)
* @return {number} (~src)
*/
PDP10.NOT = function(src)
{
/*
* Since src is a 36-bit value, we must NOT the low 32 bits separately from the higher bits,
* and then combine them with addition. Since all bits above 36 will be zero, and since ~0 is 1,
* we must mask the higher 4 bits with 0o17.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*
* Finally, all 36-bit data within a PDP-10 machine should ALWAYS be unsigned, which we now assert,
* because the divisions below would not yield correct results with negative inputs.
*/
return ((~((src / PDP10.TWO_POW32)|0) & 0o17) * PDP10.TWO_POW32) + (~src >>> 0);
};
/**
* SIGN(dst)
*
* Returns the signed form of the 36-bit operand; more efficient than doCMP(dst, 0).
*
* @param {number} dst (36-bit value)
* @return {number}
*/
PDP10.SIGN = function(dst)
{
return (dst < PDP10.INT_LIMIT? dst : dst - PDP10.WORD_LIMIT);
};
/**
* XOR(dst, src)
*
* Performs the logical "exclusive-or" (XOR) of two 36-bit operands.
*
* @param {number} dst (36-bit value)
* @param {number} src (36-bit value)
* @return {number} (dst ^ src)
*/
PDP10.XOR = function(dst, src)
{
/*
* Since dst and src are 36-bit values, we must XOR the low 32 bits separately from the higher bits,
* and then combine them with addition. Since all bits above 36 will be zero, and since 0 XOR 0 is 0,
* no special masking for the higher bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*
* Finally, all 36-bit data within a PDP-10 machine should ALWAYS be unsigned, which we now assert,
* because the divisions below would not yield correct results with negative inputs.
*/
return ((((dst / PDP10.TWO_POW32)|0) ^ ((src / PDP10.TWO_POW32)|0)) * PDP10.TWO_POW32) + ((dst ^ src) >>> 0);
};
/**
* SWAP(src)
*
* Used by callers to "swap" the left and right half-words of a 36-bit operand.
*
* NOTE: Since HALF_MASK is an 18-bit value, it's safe to use "&" with HALF_MASK (equivalent to "%" with HALF_SHIFT).
*
* @param {number} src
* @return {number} (updated src)
*/
PDP10.SWAP = function(src)
{
return ((src / PDP10.HALF_SHIFT)|0) + ((src & PDP10.HALF_MASK) * PDP10.HALF_SHIFT);
};
/**
* GETHL(op, dst, src)
*
* Used by callers to obtain HL (half-word left) with HR (half-word right) zeroed.
*
* @param {number} op
* @param {number} dst (36-bit value whose 18-bit left half is either preserved or modified)
* @param {number} src (18-bit value used to determine the sign extension, if any, for the left half of dst)
* @return {number} (updated dst)
*/
PDP10.GETHL = function(op, dst, src)
{
switch(op & 0o600) {
case 0o000:
dst -= (dst & PDP10.HALF_MASK);
break;
case 0o200:
dst = 0;
break;
case 0o400:
dst = (PDP10.HALF_MASK * PDP10.HALF_SHIFT);
break;
case 0o600:
dst = (src > PDP10.HINT_MASK? (PDP10.HALF_MASK * PDP10.HALF_SHIFT) : 0);
break;
}
return dst;
};
/**
* SETHL(op, dst, src)
*
* Used by callers to obtain HL (half-word left) with HR (half-word right) preserved.
*
* @param {number} op
* @param {number} dst (36-bit value whose 18-bit left half is either preserved or modified)
* @param {number} src (18-bit value used to determine the sign extension, if any, for the left half of dst)
* @return {number} (updated dst)
*/
PDP10.SETHL = function(op, dst, src)
{
if (op &= 0o600) {
dst &= PDP10.HALF_MASK;
switch(op) {
case 0o400:
dst += (PDP10.HALF_MASK * PDP10.HALF_SHIFT);
break;
case 0o600:
dst += (src > PDP10.HINT_MASK? (PDP10.HALF_MASK * PDP10.HALF_SHIFT) : 0);
break;
}
}
return dst;
};
/**
* GETHR(op, dst, src)
*
* Used by callers to obtain HR (half-word right) with HL (half-word left) zeroed.
*
* @param {number} op
* @param {number} dst (36-bit value whose 18-bit right half is either preserved or modified)
* @param {number} src (36-bit value used to determine the sign extension, if any, for the right half of dst)
* @return {number} (updated dst)
*/
PDP10.GETHR = function(op, dst, src)
{
switch(op & 0o600) {
case 0o000:
dst = (dst & PDP10.HALF_MASK);
break;
case 0o200:
dst = 0;
break;
case 0o400:
dst = PDP10.HALF_MASK;
break;
case 0o600:
dst = (src > PDP10.INT_MASK? PDP10.HALF_MASK : 0);
break;
}
return dst;
};
/**
* SETHR(op, dst, src)
*
* Used by callers to obtain HR (half-word right) with HL (half-word left) preserved.
*
* @param {number} op
* @param {number} dst (36-bit value whose 18-bit right half is either preserved or modified)
* @param {number} src (36-bit value used to determine the sign extension, if any, for the right half of dst)
* @return {number} (updated dst)
*/
PDP10.SETHR = function(op, dst, src)
{
if (op &= 0o600) {
dst -= (dst & PDP10.HALF_MASK);
switch(op) {
case 0o400:
dst += PDP10.HALF_MASK;
break;
case 0o600:
dst += (src > PDP10.INT_MASK? PDP10.HALF_MASK : 0);
break;
}
}
return dst;
};
/**
* ADDD(dDst, dSrc)
*
* Adds a double-length value (dSrc) to another (dDst).
*
* @param {Array.<number>} dDst
* @param {Array.<number>} dSrc
*/
PDP10.ADDD = function(dDst, dSrc)
{
dDst[0] += dSrc[0];
dDst[1] += dSrc[1];
if (dDst[0] >= PDP10.WORD_LIMIT) {
dDst[0] %= PDP10.WORD_LIMIT;
dDst[1]++;
}
};
/**
* CMPD(dDst, dSrc)
*
* Compares double-length values (dDst and dSrc) by computing dDst - dSrc.
*
* @param {Array.<number>} dDst
* @param {Array.<number>} dSrc
* @return {number} > 0 if dDst > dSrc, == 0 if dDst == dSrc, < 0 if dDst < dSrc
*/
PDP10.CMPD = function(dDst, dSrc)
{
var result = dDst[1] - dSrc[1];
if (!result) result = dDst[0] - dSrc[0];
return result;
};
/**
* INITD(dDst, lo, hi)
*
* Initializes a double-length value (dDst).
*
* @param {Array.<number>} dDst
* @param {number} lo
* @param {number} hi
*/
PDP10.INITD = function(dDst, lo, hi)
{
dDst[0] = lo;
dDst[1] = hi;
};
/**
* SHRD(dDst)
*
* Shifts a double-length value (dDst) right one bit.
*
* @param {Array.<number>} dDst
*/
PDP10.SHRD = function(dDst)
{
if (dDst[1] % 2) {
dDst[0] += PDP10.WORD_LIMIT;
}
dDst[0] = Math.trunc(dDst[0] / 2);
dDst[1] = Math.trunc(dDst[1] / 2);
};
/**
* SUBD(dDst, dSrc)
*
* Subtracts a double-length value (dSrc) from another (dDst).
*
* @param {Array.<number>} dDst
* @param {Array.<number>} dSrc
*/
PDP10.SUBD = function(dDst, dSrc)
{
dDst[0] -= dSrc[0];
dDst[1] -= dSrc[1];
if (dDst[0] < 0) {
dDst[0] += PDP10.WORD_LIMIT;
dDst[1]--;
}
};
/**
* ZEROD(d)
*
* True if all bits in the double-length value (d) are zero, false otherwise.
*
* @param {Array.<number>} d
* @return {boolean}
*/
PDP10.ZEROD = function(d)
{
return !d[0] && !d[1];
};
/*
* If we want the basic half-word operations to handle all the sub-operations; ie:
*
* None
* Zero-extend
* One-extend
* Sign-extend
*
* then we need to alias all the sub-functions to the corresponding primary functions.
*/
PDP10.opHLLZ = PDP10.opHLL;
PDP10.opHLLZI = PDP10.opHLLI;
PDP10.opHLLZM = PDP10.opHLLM;
PDP10.opHLLZS = PDP10.opHLLS;
PDP10.opHLLO = PDP10.opHLL;
PDP10.opHLLOI = PDP10.opHLLI;
PDP10.opHLLOM = PDP10.opHLLM;
PDP10.opHLLOS = PDP10.opHLLS;
PDP10.opHLLE = PDP10.opHLL;
PDP10.opHLLEI = PDP10.opHLLI;
PDP10.opHLLEM = PDP10.opHLLM;
PDP10.opHLLES = PDP10.opHLLS;
PDP10.opHRLZ = PDP10.opHRL;
PDP10.opHRLZI = PDP10.opHRLI;
PDP10.opHRLZM = PDP10.opHRLM;
PDP10.opHRLZS = PDP10.opHRLS;
PDP10.opHRLO = PDP10.opHRL;
PDP10.opHRLOI = PDP10.opHRLI;
PDP10.opHRLOM = PDP10.opHRLM;
PDP10.opHRLOS = PDP10.opHRLS;
PDP10.opHRLE = PDP10.opHRL;
PDP10.opHRLEI = PDP10.opHRLI;
PDP10.opHRLEM = PDP10.opHRLM;
PDP10.opHRLES = PDP10.opHRLS;
PDP10.opHRRZ = PDP10.opHRR;
PDP10.opHRRZI = PDP10.opHRRI;
PDP10.opHRRZM = PDP10.opHRRM;
PDP10.opHRRZS = PDP10.opHRRS;
PDP10.opHRRO = PDP10.opHRR;
PDP10.opHRROI = PDP10.opHRRI;
PDP10.opHRROM = PDP10.opHRRM;
PDP10.opHRROS = PDP10.opHRRS;
PDP10.opHRRE = PDP10.opHRR;
PDP10.opHRREI = PDP10.opHRRI;
PDP10.opHRREM = PDP10.opHRRM;
PDP10.opHRRES = PDP10.opHRRS;
PDP10.opHLRZ = PDP10.opHLR;
PDP10.opHLRZI = PDP10.opHLRI;
PDP10.opHLRZM = PDP10.opHLRM;
PDP10.opHLRZS = PDP10.opHLRS;
PDP10.opHLRO = PDP10.opHLR;
PDP10.opHLROI = PDP10.opHLRI;
PDP10.opHLROM = PDP10.opHLRM;
PDP10.opHLROS = PDP10.opHLRS;
PDP10.opHLRE = PDP10.opHLR;
PDP10.opHLREI = PDP10.opHLRI;
PDP10.opHLREM = PDP10.opHLRM;
PDP10.opHLRES = PDP10.opHLRS;
PDP10.opSETZI = PDP10.opSETZ;
PDP10.opSETOI = PDP10.opSETO;
PDP10.opSETCAI = PDP10.opSETCA;
PDP10.opSETA = PDP10.opNOP;
PDP10.opSETAI = PDP10.opNOP;
PDP10.opSETAM = PDP10.opMOVEM;
PDP10.opSETAB = PDP10.opMOVEM;
PDP10.opSETM = PDP10.opMOVE;
PDP10.opSETMI = PDP10.opMOVEI;
PDP10.opSETMM = PDP10.opNOPM;
PDP10.opSETMB = PDP10.opMOVE;
PDP10.opCAI = PDP10.opNOP;
PDP10.opCAM = PDP10.opNOPM;
PDP10.opJUMP = PDP10.opNOP;
PDP10.opTRN = PDP10.opNOP;
PDP10.opTLN = PDP10.opNOP;
PDP10.opTDN = PDP10.opNOPM;
PDP10.opTSN = PDP10.opNOPM;
PDP10.aOpXXX_KA10 = [
PDP10.opUUO, // 0o000xxx
PDP10.opUUO, // 0o001xxx
PDP10.opUUO, // 0o002xxx
PDP10.opUUO, // 0o003xxx
PDP10.opUUO, // 0o004xxx
PDP10.opUUO, // 0o005xxx
PDP10.opUUO, // 0o006xxx
PDP10.opUUO, // 0o007xxx
PDP10.opUUO, // 0o010xxx
PDP10.opUUO, // 0o011xxx
PDP10.opUUO, // 0o012xxx
PDP10.opUUO, // 0o013xxx
PDP10.opUUO, // 0o014xxx
PDP10.opUUO, // 0o015xxx
PDP10.opUUO, // 0o016xxx
PDP10.opUUO, // 0o017xxx
PDP10.opUUO, // 0o020xxx
PDP10.opUUO, // 0o021xxx
PDP10.opUUO, // 0o022xxx
PDP10.opUUO, // 0o023xxx
PDP10.opUUO, // 0o024xxx
PDP10.opUUO, // 0o025xxx
PDP10.opUUO, // 0o026xxx
PDP10.opUUO, // 0o027xxx
PDP10.opUUO, // 0o030xxx
PDP10.opUUO, // 0o031xxx
PDP10.opUUO, // 0o032xxx
PDP10.opUUO, // 0o033xxx
PDP10.opUUO, // 0o034xxx
PDP10.opUUO, // 0o035xxx
PDP10.opUUO, // 0o036xxx
PDP10.opUUO, // 0o037xxx
PDP10.opUUO, // 0o040xxx
PDP10.opUUO, // 0o041xxx
PDP10.opUUO, // 0o042xxx
PDP10.opUUO, // 0o043xxx
PDP10.opUUO, // 0o044xxx
PDP10.opUUO, // 0o045xxx
PDP10.opUUO, // 0o046xxx
PDP10.opUUO, // 0o047xxx
PDP10.opUUO, // 0o050xxx
PDP10.opUUO, // 0o051xxx
PDP10.opUUO, // 0o052xxx
PDP10.opUUO, // 0o053xxx
PDP10.opUUO, // 0o054xxx
PDP10.opUUO, // 0o055xxx
PDP10.opUUO, // 0o056xxx
PDP10.opUUO, // 0o057xxx
PDP10.opUUO, // 0o060xxx
PDP10.opUUO, // 0o061xxx
PDP10.opUUO, // 0o062xxx
PDP10.opUUO, // 0o063xxx
PDP10.opUUO, // 0o064xxx
PDP10.opUUO, // 0o065xxx
PDP10.opUUO, // 0o066xxx
PDP10.opUUO, // 0o067xxx
PDP10.opUUO, // 0o070xxx
PDP10.opUUO, // 0o071xxx
PDP10.opUUO, // 0o072xxx
PDP10.opUUO, // 0o073xxx
PDP10.opUUO, // 0o074xxx
PDP10.opUUO, // 0o075xxx
PDP10.opUUO, // 0o076xxx
PDP10.opUUO, // 0o077xxx
PDP10.opUndefined, // 0o100xxx
PDP10.opUndefined, // 0o101xxx
PDP10.opUndefined, // 0o102xxx
PDP10.opUndefined, // 0o103xxx
PDP10.opUndefined, // 0o104xxx
PDP10.opUndefined, // 0o105xxx
PDP10.opUndefined, // 0o106xxx
PDP10.opUndefined, // 0o107xxx
PDP10.opUndefined, // 0o110xxx
PDP10.opUndefined, // 0o111xxx
PDP10.opUndefined, // 0o112xxx
PDP10.opUndefined, // 0o113xxx
PDP10.opUndefined, // 0o114xxx
PDP10.opUndefined, // 0o115xxx
PDP10.opUndefined, // 0o116xxx
PDP10.opUndefined, // 0o117xxx
PDP10.opUndefined, // 0o120xxx
PDP10.opUndefined, // 0o121xxx
PDP10.opUndefined, // 0o122xxx
PDP10.opUndefined, // 0o123xxx
PDP10.opUndefined, // 0o124xxx
PDP10.opUndefined, // 0o125xxx
PDP10.opUndefined, // 0o126xxx
PDP10.opUndefined, // 0o127xxx
PDP10.opUFA, // 0o130xxx
PDP10.opDFN, // 0o131xxx
PDP10.opFSC, // 0o132xxx
PDP10.opIBP, // 0o133xxx
PDP10.opILDB, // 0o134xxx
PDP10.opLDB, // 0o135xxx
PDP10.opIDPB, // 0o136xxx
PDP10.opDPB, // 0o137xxx
PDP10.opFAD, // 0o140xxx
PDP10.opFADI, // 0o141xxx
PDP10.opFADM, // 0o142xxx
PDP10.opFADB, // 0o143xxx
PDP10.opFADR, // 0o144xxx
PDP10.opFADRI, // 0o145xxx
PDP10.opFADRM, // 0o146xxx
PDP10.opFADRB, // 0o147xxx
PDP10.opFSB, // 0o150xxx
PDP10.opFSBI, // 0o151xxx
PDP10.opFSBM, // 0o152xxx
PDP10.opFSBB, // 0o153xxx
PDP10.opFSBR, // 0o154xxx
PDP10.opFSBRI, // 0o155xxx
PDP10.opFSBRM, // 0o156xxx
PDP10.opFSBRB, // 0o157xxx
PDP10.opFMP, // 0o160xxx
PDP10.opFMPI, // 0o161xxx
PDP10.opFMPM, // 0o162xxx
PDP10.opFMPB, // 0o163xxx
PDP10.opFMPR, // 0o164xxx
PDP10.opFMPRI, // 0o165xxx
PDP10.opFMPRM, // 0o166xxx
PDP10.opFMPRB, // 0o167xxx
PDP10.opFDV, // 0o170xxx
PDP10.opFDVI, // 0o171xxx
PDP10.opFDVM, // 0o172xxx
PDP10.opFDVB, // 0o173xxx
PDP10.opFDVR, // 0o174xxx
PDP10.opFDVRI, // 0o175xxx
PDP10.opFDVRM, // 0o176xxx
PDP10.opFDVRB, // 0o177xxx
PDP10.opMOVE, // 0o200xxx
PDP10.opMOVEI, // 0o201xxx
PDP10.opMOVEM, // 0o202xxx
PDP10.opMOVES, // 0o203xxx
PDP10.opMOVS, // 0o204xxx
PDP10.opMOVSI, // 0o205xxx
PDP10.opMOVSM, // 0o206xxx
PDP10.opMOVSS, // 0o207xxx
PDP10.opMOVN, // 0o210xxx
PDP10.opMOVNI, // 0o211xxx
PDP10.opMOVNM, // 0o212xxx
PDP10.opMOVNS, // 0o213xxx
PDP10.opMOVM, // 0o214xxx
PDP10.opMOVMI, // 0o215xxx
PDP10.opMOVMM, // 0o216xxx
PDP10.opMOVMS, // 0o217xxx
PDP10.opIMUL, // 0o220xxx
PDP10.opIMULI, // 0o221xxx
PDP10.opIMULM, // 0o222xxx
PDP10.opIMULB, // 0o223xxx
PDP10.opMUL, // 0o224xxx
PDP10.opMULI, // 0o225xxx
PDP10.opMULM, // 0o226xxx
PDP10.opMULB, // 0o227xxx
PDP10.opIDIV, // 0o230xxx
PDP10.opIDIVI, // 0o231xxx
PDP10.opIDIVM, // 0o232xxx
PDP10.opIDIVB, // 0o233xxx
PDP10.opDIV, // 0o234xxx
PDP10.opDIVI, // 0o235xxx
PDP10.opDIVM, // 0o236xxx
PDP10.opDIVB, // 0o237xxx
PDP10.opASH, // 0o240xxx
PDP10.opROT, // 0o241xxx
PDP10.opLSH, // 0o242xxx
PDP10.opJFFO, // 0o243xxx
PDP10.opASHC, // 0o244xxx
PDP10.opROTC, // 0o245xxx
PDP10.opLSHC, // 0o246xxx
PDP10.opUndefined, // 0o247xxx
PDP10.opEXCH, // 0o250xxx
PDP10.opBLT, // 0o251xxx
PDP10.opAOBJP, // 0o252xxx
PDP10.opAOBJN, // 0o253xxx
PDP10.opJRST, // 0o254xxx
PDP10.opJFCL, // 0o255xxx
PDP10.opXCT, // 0o256xxx
PDP10.opUndefined, // 0o257xxx
PDP10.opPUSHJ, // 0o260xxx
PDP10.opPUSH, // 0o261xxx
PDP10.opPOP, // 0o262xxx
PDP10.opPOPJ, // 0o263xxx
PDP10.opJSR, // 0o264xxx
PDP10.opJSP, // 0o265xxx
PDP10.opJSA, // 0o266xxx
PDP10.opJRA, // 0o267xxx
PDP10.opADD, // 0o270xxx
PDP10.opADDI, // 0o271xxx
PDP10.opADDM, // 0o272xxx
PDP10.opADDB, // 0o273xxx
PDP10.opSUB, // 0o274xxx
PDP10.opSUBI, // 0o275xxx
PDP10.opSUBM, // 0o276xxx
PDP10.opSUBB, // 0o277xxx
PDP10.opCAI, // 0o300xxx
PDP10.opCAIL, // 0o301xxx
PDP10.opCAIE, // 0o302xxx
PDP10.opCAILE, // 0o303xxx
PDP10.opCAIA, // 0o304xxx
PDP10.opCAIGE, // 0o305xxx
PDP10.opCAIN, // 0o306xxx
PDP10.opCAIG, // 0o307xxx
PDP10.opCAM, // 0o310xxx
PDP10.opCAML, // 0o311xxx
PDP10.opCAME, // 0o312xxx
PDP10.opCAMLE, // 0o313xxx
PDP10.opCAMA, // 0o314xxx
PDP10.opCAMGE, // 0o315xxx
PDP10.opCAMN, // 0o316xxx
PDP10.opCAMG, // 0o317xxx
PDP10.opJUMP, // 0o320xxx
PDP10.opJUMPL, // 0o321xxx
PDP10.opJUMPE, // 0o322xxx
PDP10.opJUMPLE, // 0o323xxx
PDP10.opJUMPA, // 0o324xxx
PDP10.opJUMPGE, // 0o325xxx
PDP10.opJUMPN, // 0o326xxx
PDP10.opJUMPG, // 0o327xxx
PDP10.opSKIP, // 0o330xxx
PDP10.opSKIPL, // 0o331xxx
PDP10.opSKIPE, // 0o332xxx
PDP10.opSKIPLE, // 0o333xxx
PDP10.opSKIPA, // 0o334xxx
PDP10.opSKIPGE, // 0o335xxx
PDP10.opSKIPN, // 0o336xxx
PDP10.opSKIPG, // 0o337xxx
PDP10.opAOJ, // 0o340xxx
PDP10.opAOJL, // 0o341xxx
PDP10.opAOJE, // 0o342xxx
PDP10.opAOJLE, // 0o343xxx
PDP10.opAOJA, // 0o344xxx
PDP10.opAOJGE, // 0o345xxx
PDP10.opAOJN, // 0o346xxx
PDP10.opAOJG, // 0o347xxx
PDP10.opAOS, // 0o350xxx
PDP10.opAOSL, // 0o351xxx
PDP10.opAOSE, // 0o352xxx
PDP10.opAOSLE, // 0o353xxx
PDP10.opAOSA, // 0o354xxx
PDP10.opAOSGE, // 0o355xxx
PDP10.opAOSN, // 0o356xxx
PDP10.opAOSG, // 0o357xxx
PDP10.opSOJ, // 0o360xxx
PDP10.opSOJL, // 0o361xxx
PDP10.opSOJE, // 0o362xxx
PDP10.opSOJLE, // 0o363xxx
PDP10.opSOJA, // 0o364xxx
PDP10.opSOJGE, // 0o365xxx
PDP10.opSOJN, // 0o366xxx
PDP10.opSOJG, // 0o367xxx
PDP10.opSOS, // 0o370xxx
PDP10.opSOSL, // 0o371xxx
PDP10.opSOSE, // 0o372xxx
PDP10.opSOSLE, // 0o373xxx
PDP10.opSOSA, // 0o374xxx
PDP10.opSOSGE, // 0o375xxx
PDP10.opSOSN, // 0o376xxx
PDP10.opSOSG, // 0o377xxx
PDP10.opSETZ, // 0o400xxx
PDP10.opSETZI, // 0o401xxx
PDP10.opSETZM, // 0o402xxx
PDP10.opSETZB, // 0o403xxx
PDP10.opAND, // 0o404xxx
PDP10.opANDI, // 0o405xxx
PDP10.opANDM, // 0o406xxx
PDP10.opANDB, // 0o407xxx
PDP10.opANDCA, // 0o410xxx
PDP10.opANDCAI, // 0o411xxx
PDP10.opANDCAM, // 0o412xxx
PDP10.opANDCAB, // 0o413xxx
PDP10.opSETM, // 0o414xxx
PDP10.opSETMI, // 0o415xxx
PDP10.opSETMM, // 0o416xxx
PDP10.opSETMB, // 0o417xxx
PDP10.opANDCM, // 0o420xxx
PDP10.opANDCMI, // 0o421xxx
PDP10.opANDCMM, // 0o422xxx
PDP10.opANDCMB, // 0o423xxx
PDP10.opSETA, // 0o424xxx
PDP10.opSETAI, // 0o425xxx
PDP10.opSETAM, // 0o426xxx
PDP10.opSETAB, // 0o427xxx
PDP10.opXOR, // 0o430xxx
PDP10.opXORI, // 0o431xxx
PDP10.opXORM, // 0o432xxx
PDP10.opXORB, // 0o433xxx
PDP10.opIOR, // 0o434xxx
PDP10.opIORI, // 0o435xxx
PDP10.opIORM, // 0o436xxx
PDP10.opIORB, // 0o437xxx
PDP10.opANDCB, // 0o440xxx
PDP10.opANDCBI, // 0o441xxx
PDP10.opANDCBM, // 0o442xxx
PDP10.opANDCBB, // 0o443xxx
PDP10.opEQV, // 0o444xxx
PDP10.opEQVI, // 0o445xxx
PDP10.opEQVM, // 0o446xxx
PDP10.opEQVB, // 0o447xxx
PDP10.opSETCA, // 0o450xxx
PDP10.opSETCAI, // 0o451xxx
PDP10.opSETCAM, // 0o452xxx
PDP10.opSETCAB, // 0o453xxx
PDP10.opORCA, // 0o454xxx
PDP10.opORCAI, // 0o455xxx
PDP10.opORCAM, // 0o456xxx
PDP10.opORCAB, // 0o457xxx
PDP10.opSETCM, // 0o460xxx
PDP10.opSETCMI, // 0o461xxx
PDP10.opSETCMM, // 0o462xxx
PDP10.opSETCMB, // 0o463xxx
PDP10.opORCM, // 0o464xxx
PDP10.opORCMI, // 0o465xxx
PDP10.opORCMM, // 0o466xxx
PDP10.opORCMB, // 0o467xxx
PDP10.opORCB, // 0o470xxx
PDP10.opORCBI, // 0o471xxx
PDP10.opORCBM, // 0o472xxx
PDP10.opORCBB, // 0o473xxx
PDP10.opSETO, // 0o474xxx
PDP10.opSETOI, // 0o475xxx
PDP10.opSETOM, // 0o476xxx
PDP10.opSETOB, // 0o477xxx
PDP10.opHLL, // 0o500xxx
PDP10.opHLLI, // 0o501xxx
PDP10.opHLLM, // 0o502xxx
PDP10.opHLLS, // 0o503xxx
PDP10.opHRL, // 0o504xxx
PDP10.opHRLI, // 0o505xxx
PDP10.opHRLM, // 0o506xxx
PDP10.opHRLS, // 0o507xxx
PDP10.opHLLZ, // 0o510xxx
PDP10.opHLLZI, // 0o511xxx
PDP10.opHLLZM, // 0o512xxx
PDP10.opHLLZS, // 0o513xxx
PDP10.opHRLZ, // 0o514xxx
PDP10.opHRLZI, // 0o515xxx
PDP10.opHRLZM, // 0o516xxx
PDP10.opHRLZS, // 0o517xxx
PDP10.opHLLO, // 0o520xxx
PDP10.opHLLOI, // 0o521xxx
PDP10.opHLLOM, // 0o522xxx
PDP10.opHLLOS, // 0o523xxx
PDP10.opHRLO, // 0o524xxx
PDP10.opHRLOI, // 0o525xxx
PDP10.opHRLOM, // 0o526xxx
PDP10.opHRLOS, // 0o527xxx
PDP10.opHLLE, // 0o530xxx
PDP10.opHLLEI, // 0o531xxx
PDP10.opHLLEM, // 0o532xxx
PDP10.opHLLES, // 0o533xxx
PDP10.opHRLE, // 0o534xxx
PDP10.opHRLEI, // 0o535xxx
PDP10.opHRLEM, // 0o536xxx
PDP10.opHRLES, // 0o537xxx
PDP10.opHRR, // 0o540xxx
PDP10.opHRRI, // 0o541xxx
PDP10.opHRRM, // 0o542xxx
PDP10.opHRRS, // 0o543xxx
PDP10.opHLR, // 0o544xxx
PDP10.opHLRI, // 0o545xxx
PDP10.opHLRM, // 0o546xxx
PDP10.opHLRS, // 0o547xxx
PDP10.opHRRZ, // 0o550xxx
PDP10.opHRRZI, // 0o551xxx
PDP10.opHRRZM, // 0o552xxx
PDP10.opHRRZS, // 0o553xxx
PDP10.opHLRZ, // 0o554xxx
PDP10.opHLRZI, // 0o555xxx
PDP10.opHLRZM, // 0o556xxx
PDP10.opHLRZS, // 0o557xxx
PDP10.opHRRO, // 0o560xxx
PDP10.opHRROI, // 0o561xxx
PDP10.opHRROM, // 0o562xxx
PDP10.opHRROS, // 0o563xxx
PDP10.opHLRO, // 0o564xxx
PDP10.opHLROI, // 0o565xxx
PDP10.opHLROM, // 0o566xxx
PDP10.opHLROS, // 0o567xxx
PDP10.opHRRE, // 0o570xxx
PDP10.opHRREI, // 0o571xxx
PDP10.opHRREM, // 0o572xxx
PDP10.opHRRES, // 0o573xxx
PDP10.opHLRE, // 0o574xxx
PDP10.opHLREI, // 0o575xxx
PDP10.opHLREM, // 0o576xxx
PDP10.opHLRES, // 0o577xxx
PDP10.opTRN, // 0o600xxx
PDP10.opTLN, // 0o601xxx
PDP10.opTRNE, // 0o602xxx
PDP10.opTLNE, // 0o603xxx
PDP10.opTRNA, // 0o604xxx
PDP10.opTLNA, // 0o605xxx
PDP10.opTRNN, // 0o606xxx
PDP10.opTLNN, // 0o607xxx
PDP10.opTDN, // 0o610xxx
PDP10.opTSN, // 0o611xxx
PDP10.opTDNE, // 0o612xxx
PDP10.opTSNE, // 0o613xxx
PDP10.opTDNA, // 0o614xxx
PDP10.opTSNA, // 0o615xxx
PDP10.opTDNN, // 0o616xxx
PDP10.opTSNN, // 0o617xxx
PDP10.opTRZ, // 0o620xxx
PDP10.opTLZ, // 0o621xxx
PDP10.opTRZE, // 0o622xxx
PDP10.opTLZE, // 0o623xxx
PDP10.opTRZA, // 0o624xxx
PDP10.opTLZA, // 0o625xxx
PDP10.opTRZN, // 0o626xxx
PDP10.opTLZN, // 0o627xxx
PDP10.opTDZ, // 0o630xxx
PDP10.opTSZ, // 0o631xxx
PDP10.opTDZE, // 0o632xxx
PDP10.opTSZE, // 0o633xxx
PDP10.opTDZA, // 0o634xxx
PDP10.opTSZA, // 0o635xxx
PDP10.opTDZN, // 0o636xxx
PDP10.opTSZN, // 0o637xxx
PDP10.opTRC, // 0o640xxx
PDP10.opTLC, // 0o641xxx
PDP10.opTRCE, // 0o642xxx
PDP10.opTLCE, // 0o643xxx
PDP10.opTRCA, // 0o644xxx
PDP10.opTLCA, // 0o645xxx
PDP10.opTRCN, // 0o646xxx
PDP10.opTLCN, // 0o647xxx
PDP10.opTDC, // 0o650xxx
PDP10.opTSC, // 0o651xxx
PDP10.opTDCE, // 0o652xxx
PDP10.opTSCE, // 0o653xxx
PDP10.opTDCA, // 0o654xxx
PDP10.opTSCA, // 0o655xxx
PDP10.opTDCN, // 0o656xxx
PDP10.opTSCN, // 0o657xxx
PDP10.opTRO, // 0o660xxx
PDP10.opTLO, // 0o661xxx
PDP10.opTROE, // 0o662xxx
PDP10.opTLOE, // 0o663xxx
PDP10.opTROA, // 0o664xxx
PDP10.opTLOA, // 0o665xxx
PDP10.opTRON, // 0o666xxx
PDP10.opTLON, // 0o667xxx
PDP10.opTDO, // 0o670xxx
PDP10.opTSO, // 0o671xxx
PDP10.opTDOE, // 0o672xxx
PDP10.opTSOE, // 0o673xxx
PDP10.opTDOA, // 0o674xxx
PDP10.opTSOA, // 0o675xxx
PDP10.opTDON, // 0o676xxx
PDP10.opTSON, // 0o677xxx
PDP10.opIO, // 0o700xxx
PDP10.opIO, // 0o701xxx
PDP10.opIO, // 0o702xxx
PDP10.opIO, // 0o703xxx
PDP10.opIO, // 0o704xxx
PDP10.opIO, // 0o705xxx
PDP10.opIO, // 0o706xxx
PDP10.opIO, // 0o707xxx
PDP10.opIO, // 0o710xxx
PDP10.opIO, // 0o711xxx
PDP10.opIO, // 0o712xxx
PDP10.opIO, // 0o713xxx
PDP10.opIO, // 0o714xxx
PDP10.opIO, // 0o715xxx
PDP10.opIO, // 0o716xxx
PDP10.opIO, // 0o717xxx
PDP10.opIO, // 0o720xxx
PDP10.opIO, // 0o721xxx
PDP10.opIO, // 0o722xxx
PDP10.opIO, // 0o723xxx
PDP10.opIO, // 0o724xxx
PDP10.opIO, // 0o725xxx
PDP10.opIO, // 0o726xxx
PDP10.opIO, // 0o727xxx
PDP10.opIO, // 0o730xxx
PDP10.opIO, // 0o731xxx
PDP10.opIO, // 0o732xxx
PDP10.opIO, // 0o733xxx
PDP10.opIO, // 0o734xxx
PDP10.opIO, // 0o735xxx
PDP10.opIO, // 0o736xxx
PDP10.opIO, // 0o737xxx
PDP10.opIO, // 0o740xxx
PDP10.opIO, // 0o741xxx
PDP10.opIO, // 0o742xxx
PDP10.opIO, // 0o743xxx
PDP10.opIO, // 0o744xxx
PDP10.opIO, // 0o745xxx
PDP10.opIO, // 0o746xxx
PDP10.opIO, // 0o747xxx
PDP10.opIO, // 0o750xxx
PDP10.opIO, // 0o751xxx
PDP10.opIO, // 0o752xxx
PDP10.opIO, // 0o753xxx
PDP10.opIO, // 0o754xxx
PDP10.opIO, // 0o755xxx
PDP10.opIO, // 0o756xxx
PDP10.opIO, // 0o757xxx
PDP10.opIO, // 0o760xxx
PDP10.opIO, // 0o761xxx
PDP10.opIO, // 0o762xxx
PDP10.opIO, // 0o763xxx
PDP10.opIO, // 0o764xxx
PDP10.opIO, // 0o765xxx
PDP10.opIO, // 0o766xxx
PDP10.opIO, // 0o767xxx
PDP10.opIO, // 0o770xxx
PDP10.opIO, // 0o771xxx
PDP10.opIO, // 0o772xxx
PDP10.opIO, // 0o773xxx
PDP10.opIO, // 0o774xxx
PDP10.opIO, // 0o775xxx
PDP10.opIO, // 0o776xxx
PDP10.opIO // 0o777xxx
];
PDP10.aOpIO_KA10 = [
PDP10.opBLKI, // 0o70000x
PDP10.opDATAI, // 0o70004x
PDP10.opBLKO, // 0o70010x
PDP10.opDATAO, // 0o70014x
PDP10.opCONO, // 0o70020x
PDP10.opCONI, // 0o70024x
PDP10.opCONSZ, // 0o70030x
PDP10.opCONSO // 0o70034x
];
/**
* @copyright http://pcjs.org/modules/pdp10/lib/rom.js (C) Jeff Parsons 2012-2017
*/
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);
/**
* @copyright http://pcjs.org/modules/pdp10/lib/ram.js (C) Jeff Parsons 2012-2017
*/
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.aData = 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 = this.fReset = 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.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.aData = resource.aData;
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.aData) return;
if (this.loadImage(this.aData, 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.aData;
*/
}
this.fReset = true;
this.setReady();
}
}
/**
* reset()
*
* @this {RAMPDP10}
*/
reset()
{
if (this.fAllocated && !this.fReset) {
/*
* 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.aData) {
this.loadImage(this.aData, this.addrLoad, this.addrExec, this.addrRAM, !this.dbg);
}
}
this.fReset = false;
}
/**
* loadImage(aData, 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} aData
* @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(aData, addrLoad, addrExec, addrInit, fStart)
{
var fLoaded = false;
if (addrLoad == null) {
addrLoad = addrInit;
}
if (addrLoad != null) {
for (var i = 0; i < aData.length; i++) {
this.bus.setWordDirect(addrLoad + i, aData[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) {
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);
/**
* @copyright http://pcjs.org/modules/pdp10/lib/serial.js (C) Jeff Parsons 2012-2017
*/
/**
* 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 <textarea> control
* (so that the soft keyboard activates on iOS), instead of calling preventDefault() for
* selected keys (eg, the SPACE key, whose default behavior is to scroll the page), we must
* now call it for *all* keys, so that the keyCode isn't added to the control immediately,
* on top of whatever the machine is echoing back, resulting in double characters.
*/
if (event.preventDefault) event.preventDefault();
}
return true;
};
control.onpaste = function onKeyPress(event) {
if (event.stopPropagation) event.stopPropagation();
if (event.preventDefault) event.preventDefault();
var clipboardData = event.clipboardData || window.clipboardData;
if (clipboardData) {
/*
* NOTE: Multiple lines of pasted text will (at least on macOS) contain LFs instead of CRs;
* this is dealt with in receiveData() whenever it receives a string of characters.
*/
serial.receiveData(clipboardData.getData('Text'));
}
};
/*
* Now that we've added an onkeypress handler that calls preventDefault() for ALL keys, the control
* itself no longer needs the "readonly" attribute; we primarily need to remove it for iOS browsers,
* so that the soft keyboard will activate, but it shouldn't hurt to remove the attribute for all browsers.
*/
control.removeAttribute("readonly");
return true;
default:
break;
}
return false;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {SerialPortPDP10}
* @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.setReady();
}
/**
* initConnection(fNullModem)
*
* If a machine 'connection' parameter exists of the form "{sourcePort}->{targetMachine}.{targetPort}",
* and "{sourcePort}" matches our idComponent, then look for a component with id "{targetMachine}.{targetPort}".
*
* If the target component is found, then verify that it has exported functions with the following names:
*
* receiveData(data): called when we have data to transmit; aliased internally to sendData(data)
* receiveStatus(pins): called when our control signals have changed; aliased internally to updateStatus(pins)
*
* For now, we're not going to worry about communication in the other direction, because when the target component
* performs its own initConnection(), it will find our receiveData() and receiveStatus() functions, at which point
* communication in both directions should be established, and the circle of life complete.
*
* For added robustness, if the target machine initializes much more slowly than we do, and our connection attempt
* fails, that's OK, because when it finally initializes, its initConnection() will call our initConnection();
* if we've already initialized, no harm done.
*
* @this {SerialPortPDP10}
* @param {boolean} [fNullModem] (caller's null-modem setting, to ensure our settings are in agreement)
*/
initConnection(fNullModem)
{
if (!this.connection) {
var sConnection = this.cmp.getMachineParm("connection");
if (sConnection) {
var asParts = sConnection.split('->');
if (asParts.length == 2) {
var sSourceID = Str.trim(asParts[0]);
if (sSourceID != this.idComponent) return; // this connection string is intended for another instance
var sTargetID = Str.trim(asParts[1]);
this.connection = Component.getComponentByID(sTargetID);
if (this.connection) {
var exports = this.connection['exports'];
if (exports) {
var fnConnect = exports['connect'];
if (fnConnect) fnConnect.call(this.connection, this.fNullModem);
this.sendData = exports['receiveData'];
if (this.sendData) {
this.fNullModem = fNullModem;
this.updateStatus = exports['receiveStatus'];
this.status("Connected " + this.idMachine + '.' + sSourceID + " to " + sTargetID);
return;
}
}
}
}
/*
* Changed from notice() to status() because sometimes a connection fails simply because one of us is a laggard.
*/
this.status("Unable to establish connection: " + sConnection);
}
}
}
/**
* powerUp(data, fRepower)
*
* @this {SerialPortPDP10}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @return {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
/*
* This is as late as we can currently wait to make our first inter-machine connection attempt;
* even so, the target machine's initialization process may still be ongoing, so any connection
* may be not fully resolved until the target machine performs its own initConnection(), which will
* in turn invoke our initConnection() again.
*/
this.initConnection(this.fNullModem);
if (!data) {
this.reset();
} else {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {SerialPortPDP10}
* @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 {SerialPortPDP10}
*/
reset()
{
this.initState();
}
/**
* save()
*
* This implements save support for the SerialPort component.
*
* @this {SerialPortPDP10}
* @return {Object}
*/
save()
{
var state = new State(this);
state.set(0, this.saveRegisters());
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the SerialPort component.
*
* @this {SerialPortPDP10}
* @param {Object} data
* @return {boolean} true if successful, false if failure
*/
restore(data)
{
return this.initState(data[0]);
}
/**
* initState(a)
*
* @this {SerialPortPDP10}
* @param {Array} [a]
* @return {boolean} true if successful, false if failure
*/
initState(a)
{
return true;
}
/**
* saveRegisters()
*
* Basically, the inverse of initState().
*
* @this {SerialPortPDP10}
* @return {Array}
*/
saveRegisters()
{
return [];
}
/**
* receiveData(data)
*
* This replaces the old sendRBR() function, which expected an Array of bytes. We still support that,
* but in order to support connections with other SerialPort components (ie, the PC8080 SerialPort), we
* have added support for numbers and strings as well.
*
* @this {SerialPortPDP10}
* @param {number|string|Array} data
* @return {boolean} true if received, false if not
*/
receiveData(data)
{
if (typeof data == "number") {
this.abReceive.push(data);
}
else if (typeof data == "string") {
var bASCII = 0, bASCIIPrev;
for (var i = 0; i < data.length; i++) {
bASCIIPrev = bASCII;
bASCII = data.charCodeAt(i);
/*
* NOTE: Multiple lines of pasted text will (at least on macOS) contain LFs instead of CRs;
* we convert them to CRs below. Windows may do something different, but in the worst case,
* even if we receive CR/LF pairs, this code should keep the CRs and lose the LFs.
*/
if (bASCII == Str.ASCII.LF) {
if (bASCIIPrev == Str.ASCII.CR) continue;
bASCII = Str.ASCII.CR;
}
this.abReceive.push(bASCII);
}
}
else {
this.abReceive = this.abReceive.concat(data);
}
return true; // for now, return true regardless, since we're buffering everything anyway
}
/**
* receiveByte()
*
* @this {SerialPortPDP10}
* @return {number} (0x00-0xff if byte available, -1 if not)
*/
receiveByte()
{
var b = -1;
if (this.abReceive.length) {
/*
* Here, as elsewhere (eg, the PC11 component), even if I trusted all incoming data
* to be byte values (which I don't), there's also the risk that it could be signed data
* (eg, -128 to 127, instead of 0 to 255). Both risks are good reasons to always mask
* the data assigned to RBUF with 0xff.
*/
b = this.abReceive.shift() & 0xff;
this.printMessage("receiveByte(" + Str.toHexByte(b) + ")");
if (this.fUpperCase) {
/*
* Automatically transform lower-case ASCII codes to upper-case; fUpperCase should
* only be set when a terminal or some sort of pseudo-display is being used and we don't
* trust it to have its CAPS-LOCK setting correct.
*/
if (b >= 0x61 && b < 0x7A) b -= 0x20;
}
}
return b;
}
/**
* receiveStatus(pins)
*
* @this {SerialPortPDP10}
* @param {number} pins
*/
receiveStatus(pins)
{
}
/**
* setConnection(component, fn)
*
* @this {SerialPortPDP10}
* @param {Object|null} component
* @param {function(number)} fn
* @return {boolean}
*/
setConnection(component, fn)
{
if (!this.connection) {
this.connection = component;
this.sendData = fn;
return true;
}
return false;
}
/**
* transmitByte(b)
*
* @this {SerialPortPDP10}
* @param {number} b
* @return {boolean} true if transmitted, false if not
*/
transmitByte(b)
{
var fTransmitted = false;
if (MAXDEBUG) this.printMessage("transmitByte(" + Str.toHexByte(b) + ")");
if (this.sendData) {
if (this.sendData.call(this.connection, b)) {
fTransmitted = true;
}
}
/*
* TODO: Why do DEC diagnostics like to output bytes with bit 7 set?
*/
b &= 0x7F;
if (this.controlIOBuffer) {
if (b == 0x0D) {
this.iLogicalCol = 0;
}
else if (b == 0x08) {
this.controlIOBuffer.value = this.controlIOBuffer.value.slice(0, -1);
/*
* TODO: Back up the correct number of columns if the character erased was a tab.
*/
if (this.iLogicalCol > 0) this.iLogicalCol--;
}
else if (b) {
/*
* RT-11 outputs lots of NULL characters, at least after a "D 56=5015" (0x0A0D) command has
* been issued, hence the "if (b)" check above.
*
* TODO: Also consider a check for Keys.ASCII.CTRL_C, because by default, RT-11 outputs "raw"
* CTRL_C characters, which we capture below and render as <ETX>. RT-11 does this for other keys
* as well, such as CTRL_K (<VT>) and CTRL_L (<FF>).
*/
var s = Str.toASCIICode(b); // formerly: String.fromCharCode(b);
var nChars = s.length; // formerly: (b >= 0x20? 1 : 0);
if (b < 0x20 && nChars == 1) nChars = 0;
if (b == 0x09) {
var tabSize = this.tabSize || 8;
nChars = tabSize - (this.iLogicalCol % tabSize);
if (this.tabSize) s = Str.pad("", nChars);
}
if (this.charBOL && !this.iLogicalCol && nChars) s = String.fromCharCode(this.charBOL) + s;
this.controlIOBuffer.value += s;
this.controlIOBuffer.scrollTop = this.controlIOBuffer.scrollHeight;
this.iLogicalCol += nChars;
}
fTransmitted = true;
}
else if (this.consoleOutput != null) {
if (b == 0x0A || this.consoleOutput.length >= 1024) {
this.println(this.consoleOutput);
this.consoleOutput = "";
}
if (b != 0x0A) {
this.consoleOutput += String.fromCharCode(b);
}
fTransmitted = true;
}
return fTransmitted;
}
/**
* SerialPortPDP10.init()
*
* This function operates on every HTML element of class "serial", extracting the
* JSON-encoded parameters for the SerialPort constructor from the element's "data-value"
* attribute, invoking the constructor to create a SerialPort component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
var aeSerial = Component.getElementsByClass(document, PDP10.APPCLASS, "serial");
for (var iSerial = 0; iSerial < aeSerial.length; iSerial++) {
var eSerial = aeSerial[iSerial];
var parmsSerial = Component.getComponentParms(eSerial);
var serial = new SerialPortPDP10(parmsSerial);
Component.bindComponentControls(serial, eSerial, PDP10.APPCLASS);
}
}
}
/*
* Internal name used for the I/O buffer control, if any, that we bind to the SerialPort.
*
* Alternatively, if SerialPort wants to use another component's control (eg, the Panel's
* "print" control), it can specify the name of that control with the 'binding' property.
*
* For that binding to succeed, we also need to know the target component; for now, that's
* been hard-coded to "Panel", in part because that's one of the few components we can rely
* upon initializing before we do, but it would be a simple matter to include a component type
* or ID as part of the 'binding' property as well, if we need more flexibility later.
*/
SerialPortPDP10.sIOBuffer = "buffer";
/*
* Initialize every SerialPort module on the page.
*/
Web.onInit(SerialPortPDP10.init);
/**
* @copyright http://pcjs.org/modules/shared/lib/debugger.js (C) Jeff Parsons 2012-2017
*/
/**
* Debugger Address Object
*
* This is the basic structure; other debuggers may extend it.
*
* addr address
* fTemporary true if this is a temporary breakpoint address
* sCmd set for breakpoint addresses if there's an associated command string
* aCmds preprocessed commands (from sCmd)
*
* @typedef {{
* addr:(number|undefined),
* fTemporary:(boolean|undefined),
* sCmd:(string|undefined),
* aCmds:(Array.<string>|undefined)
* }}
*/
var DbgAddr;
/**
* 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
* subclass (eg, SerialPort), 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 Debugger extends Component
{
/**
* Debugger(parmsDbg)
*
* The Debugger component supports the following optional (parmsDbg) properties:
*
* base: the base to use for most numeric input/output (default is 16)
*
* The Debugger component is a shared component containing a subset of functionality used by
* the other CPU-specific Debuggers (eg, DebuggerX86). Over time, the goal is to factor out as
* much common debugging support as possible from those components into this one.
*
* @param {Object} parmsDbg
*/
constructor(parmsDbg)
{
if (DEBUGGER) {
super("Debugger", parmsDbg);
/*
* Default base used to display all values; modified with the "s base" command.
*/
this.nBase = +parmsDbg['base'] || 16;
/*
* Default number of bits of integer precision; it can be overridden by the Debugger
* but there is no command to adjust it.
*/
this.nBits = 32;
this.achGroup = ['{','}'];
this.achAddress = ['[',']'];
/*
* These keep track of instruction activity, but only when tracing or when Debugger checks
* have been enabled (eg, one or more breakpoints have been set).
*
* They are zeroed by the reset() notification handler. cInstructions is advanced by
* stepCPU() and checkInstruction() calls. nCycles is updated by every stepCPU() or stop()
* call and simply represents the number of cycles performed by the last run of instructions.
*/
this.nCycles = 0;
this.cOpcodes = this.cOpcodesStart = 0;
/*
* fAssemble is true when "assemble mode" is active, false when not.
*/
this.fAssemble = false;
/*
* This maintains command history. New commands are inserted at index 0 of the array.
* When Enter is pressed on an empty input buffer, we default to the command at aPrevCmds[0].
*/
this.iPrevCmd = -1;
this.aPrevCmds = [];
/*
* aVariables is an object with properties that grow as setVariable() assigns more variables;
* each property corresponds to one variable, where the property name is the variable name (ie,
* a string beginning with a non-digit, followed by zero or more symbol characters and/or digits)
* and the property value is the variable's numeric value. See doVar() and setVariable() for
* details.
*
* Note that parseValue() parses variables before numbers, so any variable that looks like a
* unprefixed hex value (eg, "a5" as opposed to "0xa5") will trump the numeric value. Unprefixed
* hex values are a convenience of parseValue(), which always calls Str.parseInt() with a default
* base of 16; however, that default be overridden with a variety of explicit prefixes or suffixes
* (eg, a leading "0o" to indicate octal, a trailing period to indicate decimal, etc.)
*
* See Str.parseInt() for more details about supported numbers.
*/
this.aVariables = {};
} // endif DEBUGGER
}
/**
* getRegIndex(sReg, off)
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {string} sReg
* @param {number} [off] optional offset into sReg
* @return {number} register index, or -1 if not found
*/
getRegIndex(sReg, off)
{
return -1;
}
/**
* getRegValue(iReg)
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {number} iReg
* @return {number|undefined}
*/
getRegValue(iReg)
{
return undefined;
}
/**
* parseAddrReference(s, sAddr)
*
* Returns the given string with the given address reference replaced with the contents of that address.
*
* NOTE: This must be implemented by the individual debuggers.
*
* @this {Debugger}
* @param {string} s
* @param {string} sAddr
* @return {string}
*/
parseAddrReference(s, sAddr)
{
return s.replace('[' + sAddr + ']', "unimplemented");
}
/**
* getNextCommand()
*
* @this {Debugger}
* @return {string}
*/
getNextCommand()
{
var sCmd;
if (this.iPrevCmd > 0) {
sCmd = this.aPrevCmds[--this.iPrevCmd];
} else {
sCmd = "";
this.iPrevCmd = -1;
}
return sCmd;
}
/**
* getPrevCommand()
*
* @this {Debugger}
* @return {string|null}
*/
getPrevCommand()
{
var sCmd = null;
if (this.iPrevCmd < this.aPrevCmds.length - 1) {
sCmd = this.aPrevCmds[++this.iPrevCmd];
}
return sCmd;
}
/**
* parseCommand(sCmd, fSave, chSep)
*
* @this {Debugger}
* @param {string|undefined} sCmd
* @param {boolean} [fSave] is true to save the command, false if not
* @param {string} [chSep] is the command separator character (default is ';')
* @return {Array.<string>}
*/
parseCommand(sCmd, fSave, chSep)
{
if (fSave) {
if (!sCmd) {
if (this.fAssemble) {
sCmd = "end";
} else {
sCmd = this.aPrevCmds[this.iPrevCmd+1];
}
} else {
if (this.iPrevCmd < 0 && this.aPrevCmds.length) {
this.iPrevCmd = 0;
}
if (this.iPrevCmd < 0 || sCmd != this.aPrevCmds[this.iPrevCmd]) {
this.aPrevCmds.splice(0, 0, sCmd);
this.iPrevCmd = 0;
}
this.iPrevCmd--;
}
}
var a = [];
if (sCmd) {
/*
* With the introduction of breakpoint commands (ie, quoted command sequences
* associated with a breakpoint), we can no longer perform simplistic splitting.
*
* a = sCmd.split(chSep || ';');
* for (var i = 0; i < a.length; i++) a[i] = Str.trim(a[i]);
*
* We may now split on semi-colons ONLY if they are outside a quoted sequence.
*
* Also, to allow quoted strings *inside* breakpoint commands, we first replace all
* DOUBLE double-quotes with single quotes.
*/
sCmd = sCmd.replace(/""/g, "'");
var iPrev = 0;
var chQuote = null;
chSep = chSep || ';';
/*
* NOTE: Processing charAt() up to and INCLUDING length is not a typo; we're taking
* advantage of the fact that charAt() with an invalid index returns an empty string,
* allowing us to use the same substring() call to capture the final portion of sCmd.
*
* In a sense, it allows us to pretend that the string ends with a zero terminator.
*/
for (var i = 0; i <= sCmd.length; i++) {
var ch = sCmd.charAt(i);
if (ch == '"' || ch == "'") {
if (!chQuote) {
chQuote = ch;
} else if (ch == chQuote) {
chQuote = null;
}
}
else if (ch == chSep && !chQuote || !ch) {
/*
* Recall that substring() accepts starting (inclusive) and ending (exclusive)
* indexes, whereas substr() accepts a starting index and a length. We need the former.
*/
a.push(Str.trim(sCmd.substring(iPrev, i)));
iPrev = i + 1;
}
}
}
return a;
}
/**
* evalAND(dst, src)
*
* Adapted from /modules/pdp10/lib/cpuops.js:PDP10.AND().
*
* Performs the bitwise "and" (AND) of two operands > 32 bits.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst & src)
*/
evalAND(dst, src)
{
/*
* We AND the low 32 bits separately from the higher bits, and then combine them with addition.
* Since all bits above 32 will be zero, and since 0 AND 0 is 0, no special masking for the higher
* bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*/
if (this.nBits <= 32) {
return dst & src;
}
/*
* Negative values don't yield correct results when dividing, so pass them through an unsigned truncate().
*/
dst = this.truncate(dst, 0, true);
src = this.truncate(src, 0, true);
return ((((dst / Debugger.TWO_POW32)|0) & ((src / Debugger.TWO_POW32)|0)) * Debugger.TWO_POW32) + ((dst & src) >>> 0);
}
/**
* evalIOR(dst, src)
*
* Adapted from /modules/pdp10/lib/cpuops.js:PDP10.IOR().
*
* Performs the logical "inclusive-or" (OR) of two operands > 32 bits.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst | src)
*/
evalIOR(dst, src)
{
/*
* We OR the low 32 bits separately from the higher bits, and then combine them with addition.
* Since all bits above 32 will be zero, and since 0 OR 0 is 0, no special masking for the higher
* bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*/
if (this.nBits <= 32) {
return dst | src;
}
/*
* Negative values don't yield correct results when dividing, so pass them through an unsigned truncate().
*/
dst = this.truncate(dst, 0, true);
src = this.truncate(src, 0, true);
return ((((dst / Debugger.TWO_POW32)|0) | ((src / Debugger.TWO_POW32)|0)) * Debugger.TWO_POW32) + ((dst | src) >>> 0);
}
/**
* evalXOR(dst, src)
*
* Adapted from /modules/pdp10/lib/cpuops.js:PDP10.XOR().
*
* Performs the logical "exclusive-or" (XOR) of two operands > 32 bits.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst ^ src)
*/
evalXOR(dst, src)
{
/*
* We XOR the low 32 bits separately from the higher bits, and then combine them with addition.
* Since all bits above 32 will be zero, and since 0 XOR 0 is 0, no special masking for the higher
* bits is required.
*
* WARNING: When using JavaScript's 32-bit operators with values that could set bit 31 and produce a
* negative value, it's critical to perform a final right-shift of 0, ensuring that the final result is
* positive.
*/
if (this.nBits <= 32) {
return dst | src;
}
/*
* Negative values don't yield correct results when dividing, so pass them through an unsigned truncate().
*/
dst = this.truncate(dst, 0, true);
src = this.truncate(src, 0, true);
return ((((dst / Debugger.TWO_POW32)|0) ^ ((src / Debugger.TWO_POW32)|0)) * Debugger.TWO_POW32) + ((dst ^ src) >>> 0);
}
/**
* evalMUL(dst, src)
*
* I could have adapted the code from /modules/pdp10/lib/cpuops.js:PDP10.doMUL(), but it was simpler to
* write this base method and let the PDP-10 Debugger override it with a call to the *actual* doMUL() method.
*
* @this {Debugger}
* @param {number} dst
* @param {number} src
* @return {number} (dst * src)
*/
evalMUL(dst, src)
{
return dst * src;
}
/**
* truncate(v, nBits, fUnsigned)
*
* @this {Debugger}
* @param {number} v
* @param {number} [nBits]
* @param {boolean} [fUnsigned]
* @return {number}
*/
truncate(v, nBits, fUnsigned)
{
var limit, vNew = v;
nBits = nBits || this.nBits;
if (fUnsigned) {
if (nBits == 32) {
vNew = v >>> 0;
}
else if (nBits < 32) {
vNew = v & ((1 << nBits) - 1);
}
else {
limit = Math.pow(2, nBits);
if (v < 0 || v >= limit) {
vNew = v % limit;
if (vNew < 0) vNew += limit;
}
}
}
else {
if (nBits <= 32) {
vNew = (v << (32 - nBits)) >> (32 - nBits);
}
else {
limit = Math.pow(2, nBits - 1);
if (v >= limit) {
vNew = (v % limit);
if (((v / limit)|0) & 1) vNew -= limit;
} else if (v < -limit) {
vNew = (v % limit);
if ((((-v - 1) / limit) | 0) & 1) {
if (vNew) vNew += limit;
}
else {
if (!vNew) vNew -= limit;
}
}
}
}
if (v != vNew) {
if (MAXDEBUG) this.println("warning: value " + v + " truncated to " + vNew);
v = vNew;
}
return v;
}
/**
* evalOps(aVals, aOps, cOps)
*
* Some of our clients want a specific number of bits of integer precision. If that precision is
* greater than 32, some of the operations below will fail; for example, JavaScript bitwise operators
* always truncate the result to 32 bits, so beware when using shift operations. Similarly, it would
* be wrong to always "|0" the final result, which is why we rely on truncate() now.
*
* Note that JavaScript integer precision is limited to 52 bits. For example, in Node, if you set a
* variable to 0x80000001:
*
* foo=0x80000001|0
*
* then calculate foo*foo and display the result in binary using "(foo*foo).toString(2)":
*
* '11111111111111111111111111111100000000000000000000000000000000'
*
* which is slightly incorrect because it has overflowed JavaScript's floating-point precision.
*
* 0x80000001 in decimal is -2147483647, so the product is 4611686014132420609, which is 0x3FFFFFFF00000001.
*
* @this {Debugger}
* @param {Array.<number>} aVals
* @param {Array.<string>} aOps
* @param {number} [cOps] (default is -1 for all)
* @return {boolean} true if successful, false if error
*/
evalOps(aVals, aOps, cOps = -1)
{
while (cOps-- && aOps.length) {
var chOp = aOps.pop();
if (aVals.length < 2) return false;
var valNew;
var val2 = aVals.pop();
var val1 = aVals.pop();
switch(chOp) {
case '*':
valNew = this.evalMUL(val1, val2);
break;
case '/':
if (!val2) return false;
valNew = Math.trunc(val1 / val2);
break;
case '^/':
if (!val2) return false;
valNew = val1 % val2;
break;
case '+':
valNew = val1 + val2;
break;
case '-':
valNew = val1 - val2;
break;
case '<<':
valNew = val1 << val2;
break;
case '>>':
valNew = val1 >> val2;
break;
case '>>>':
valNew = val1 >>> val2;
break;
case '<':
valNew = (val1 < val2? 1 : 0);
break;
case '<=':
valNew = (val1 <= val2? 1 : 0);
break;
case '>':
valNew = (val1 > val2? 1 : 0);
break;
case '>=':
valNew = (val1 >= val2? 1 : 0);
break;
case '==':
valNew = (val1 == val2? 1 : 0);
break;
case '!=':
valNew = (val1 != val2? 1 : 0);
break;
case '&':
valNew = this.evalAND(val1, val2);
break;
case '!': // alias for MACRO-10 to perform a bitwise inclusive-or (OR)
case '|':
valNew = this.evalIOR(val1, val2);
break;
case '^!': // since MACRO-10 uses '^' for base overrides, '^!' is used for bitwise exclusive-or (XOR)
valNew = this.evalXOR(val1, val2);
break;
case '&&':
valNew = (val1 && val2? 1 : 0);
break;
case '||':
valNew = (val1 || val2? 1 : 0);
break;
case ',,':
valNew = this.truncate(val1, 18, true) * Math.pow(2, 18) + this.truncate(val2, 18, true);
break;
case '_':
case '^_':
valNew = val1;
/*
* While we always try to avoid assuming any particular number of bits of precision, the 'B' shift
* operator (which we've converted to '^_') is unique to the MACRO-10 environment, which imposes the
* following restrictions on the shift count.
*/
if (chOp == '^_') val2 = 35 - (val2 & 0xff);
if (val2) {
/*
* Since binary shifting is a logical (not arithmetic) operation, and since shifting by division only
* works properly with positive numbers, we call truncate() to produce an unsigned value.
*/
valNew = this.truncate(valNew, 0, true);
if (val2 > 0) {
valNew *= Math.pow(2, val2);
} else {
valNew = Math.trunc(valNew / Math.pow(2, -val2));
}
}
break;
default:
return false;
}
aVals.push(this.truncate(valNew));
}
return true;
}
/**
* parseArray(asValues, iValue, iLimit, nBase, aUndefined)
*
* parseExpression() takes a complete expression and divides it into array elements, where even elements
* are values (which may be empty if two or more operators appear consecutively) and odd elements are operators.
*
* For example, if the original expression was "2*{3+{4/2}}", parseExpression() would call parseArray() with:
*
* 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
* - - - - - - - - - - -- -- -- -- --
* 2 * { 3 + { 4 / 2 } }
*
* This function takes care of recursively processing grouped expressions, by processing subsets of the array,
* as well as handling certain base overrides (eg, temporarily switching to base-10 for binary shift suffixes).
*
* @param {Array.<string>} asValues
* @param {number} iValue
* @param {number} iLimit
* @param {number} nBase
* @param {Array|undefined} [aUndefined]
* @return {number|undefined}
*/
parseArray(asValues, iValue, iLimit, nBase, aUndefined)
{
var value;
var sValue, sOp;
var fError = false;
var nUnary = 0;
var aVals = [], aOps = [];
var nBasePrev = this.nBase;
this.nBase = nBase;
while (iValue < iLimit) {
var v;
sValue = asValues[iValue++].trim();
sOp = (iValue < iLimit? asValues[iValue++] : "");
if (sValue) {
v = this.parseValue(sValue, null, aUndefined, nUnary);
} else {
if (sOp == '{') {
var cOpen = 1;
var iStart = iValue;
while (iValue < iLimit) {
sValue = asValues[iValue++].trim();
sOp = (iValue < asValues.length? asValues[iValue++] : "");
if (sOp == '{') {
cOpen++;
} else if (sOp == '}') {
if (!--cOpen) break;
}
}
v = this.parseArray(asValues, iStart, iValue-1, this.nBase, aUndefined);
if (v != null && nUnary) {
v = this.parseUnary(v, nUnary);
}
sValue = (iValue < iLimit? asValues[iValue++].trim() : "");
sOp = (iValue < iLimit? asValues[iValue++] : "");
}
else {
/*
* When parseExpression() calls us, it has collapsed all runs of whitespace into single spaces,
* and although it allows single spaces to divide the elements of the expression, a space is neither
* a unary nor binary operator. It's essentially a no-op. If we encounter it here, then it followed
* another operator and is easily ignored (although perhaps it should still trigger a reset of nBase
* and nUnary -- TBD).
*/
if (sOp == ' ') {
continue;
}
if (sOp == '^B') {
this.nBase = 2;
continue;
}
if (sOp == '^O') {
this.nBase = 8;
continue;
}
if (sOp == '^D') {
this.nBase = 10;
continue;
}
if (!(nUnary & (0xC0000000|0))) {
if (sOp == '+') {
continue;
}
if (sOp == '-') {
nUnary = (nUnary << 2) | 1;
continue;
}
if (sOp == '~' || sOp == '^-') {
nUnary = (nUnary << 2) | 2;
continue;
}
if (sOp == '^L') {
nUnary = (nUnary << 2) | 3;
continue;
}
}
fError = true;
break;
}
}
if (v === undefined) {
if (aUndefined) {
aUndefined.push(sValue);
v = 0;
} else {
fError = true;
aUndefined = [];
break;
}
}
aVals.push(this.truncate(v));
/*
* When parseExpression() calls us, it has collapsed all runs of whitespace into single spaces,
* and although it allows single spaces to divide the elements of the expression, a space is neither
* a unary nor binary operator. It's essentially a no-op. If we encounter it here, then it followed
* a value, and since we don't want to misinterpret the next operator as a unary operator, we look
* ahead and grab the next operator if it's not preceded by a value.
*/
if (sOp == ' ') {
if (iValue < asValues.length - 1 && !asValues[iValue]) {
iValue++;
sOp = asValues[iValue++]
} else {
fError = true;
break;
}
}
if (!sOp) break;
var aBinOp = (this.achGroup[0] == '<'? Debugger.aDECOpPrecedence : Debugger.aBinOpPrecedence);
if (!aBinOp[sOp]) {
fError = true;
break;
}
if (aOps.length && aBinOp[sOp] <= aBinOp[aOps[aOps.length - 1]]) {
this.evalOps(aVals, aOps, 1);
}
aOps.push(sOp);
/*
* The MACRO-10 binary shifting operator assumes a base-10 shift count, regardless of the current
* base, so we must override the current base to ensure the count is parsed correctly.
*/
this.nBase = (sOp == '^_')? 10 : nBase;
nUnary = 0;
}
if (fError || !this.evalOps(aVals, aOps) || aVals.length != 1) {
fError = true;
}
if (!fError) {
value = aVals.pop();
} else if (!aUndefined) {
this.println("parse error (" + (sValue || sOp) + ")");
}
this.nBase = nBasePrev;
return value;
}
/**
* parseASCII(sExp, chDelim, nBits, cchMax)
*
* @this {Debugger}
* @param {string} sExp
* @param {string} chDelim
* @param {number} nBits
* @param {number} cchMax
* @return {string|undefined}
*/
parseASCII(sExp, chDelim, nBits, cchMax)
{
var i;
while ((i = sExp.indexOf(chDelim)) >= 0) {
var v = 0;
var j = i + 1;
var cch = cchMax;
while (j < sExp.length) {
var ch = sExp[j++];
if (ch == chDelim) {
cch = -1;
break;
}
if (!cch) break;
cch--;
var c = ch.charCodeAt(0);
if (nBits == 7) {
c &= 0x7F;
} else {
c = (c - 0x20) & 0x3F;
}
v = this.truncate(v * Math.pow(2, nBits) + c, nBits * cchMax, true);
}
if (cch >= 0) {
this.println("parse error (" + chDelim + sExp + chDelim + ")");
return undefined;
} else {
sExp = sExp.substr(0, i) + this.toStrBase(v, -1) + sExp.substr(j);
}
}
return sExp;
}
/**
* parseExpression(sExp, fQuiet)
*
* A quick-and-dirty expression parser. It takes an expression like:
*
* EDX+EDX*4+12345678
*
* and builds a value stack in aVals and a "binop" (binary operator) stack in aOps:
*
* aVals aOps
* ----- ----
* EDX +
* EDX *
* 4 +
* ...
*
* We pop 1 "binop" from aOps and 2 values from aVals whenever a "binop" of lower priority than its
* predecessor is encountered, evaluate, and push the result back onto aVals. Only selected unary
* operators are supported (eg, negate and complement); no ternary operators like '?:' are supported.
*
* fQuiet can be used to pass an array that collects any undefined variables that parseExpression()
* encounters; the value of an undefined variable is zero. This mode was added for components that need
* to support expressions containing "fixups" (ie, values that must be determined later).
*
* @this {Debugger}
* @param {string|undefined} sExp
* @param {Array|undefined|boolean} [fQuiet]
* @return {number|undefined} numeric value, or undefined if sExp contains any undefined or invalid values
*/
parseExpression(sExp, fQuiet)
{
var value = undefined;
var fPrint = (fQuiet === false);
var aUndefined = Array.isArray(fQuiet)? fQuiet : undefined;
if (sExp) {
/*
* The default delimiting characters for grouped expressions are braces; they can be changed by altering
* achGroup, but when that happens, instead of changing our regular expressions and operator tables,
* we simply replace all achGroup characters with braces in the given expression.
*
* Why not use parentheses for grouped expressions? Because some debuggers use parseReference() to perform
* parenthetical value replacements in message strings, and they don't want parentheses taking on a different
* meaning. And for some machines, like the PDP-10, the convention is to use parentheses for other things,
* like indexed addressing, and to use angle brackets for grouped expressions.
*/
if (this.achGroup[0] != '{') {
sExp = sExp.split(this.achGroup[0]).join('{').split(this.achGroup[1]).join('}');
}
/*
* Quoted ASCII characters can have a numeric value, too, which must be converted now, to avoid any
* conflicts with the operators below.
*/
sExp = this.parseASCII(sExp, '"', 7, 5); // MACRO-10 packs up to 5 7-bit ASCII codes into a value
if (!sExp) return value;
sExp = this.parseASCII(sExp, "'", 6, 6); // MACRO-10 packs up to 6 6-bit ASCII (SIXBIT) codes into a value
if (!sExp) return value;
/*
* All browsers (including, I believe, IE9 and up) support the following idiosyncrasy of a RegExp split():
* when the RegExp uses a capturing pattern, the resulting array will include entries for all the pattern
* matches along with the non-matches. This effectively means that, in the set of expressions that we
* support, all even entries in asValues will contain "values" and all odd entries will contain "operators".
*
* Although I started listing the operators in the RegExp in "precedential" order, that's not important;
* what IS important is listing operators than contain shorter operators first. For example, bitwise
* shift operators must be listed BEFORE the logical less-than or greater-than operators. The aBinOp tables
* (aBinOpPrecedence and aDECOpPrecedence) are what determine precedence, not the RegExp.
*
* Also, to better accommodate MACRO-10 syntax, I've replaced the single '^' for XOR with '^!', and I've
* added '!' as an alias for '|' (bitwise inclusive-or), '^-' as an alias for '~' (one's complement operator),
* and '_' as a shift operator (+/- values specify a left/right shift, and the count is not limited to 32).
*
* And to avoid conflicts with MACRO-10 syntax, I've replaced the original mod operator ('%') with '^/'.
*
* The MACRO-10 binary shifting suffix ('B') is a bit more problematic, since a capital B can also appear
* inside symbols, or inside hex values. So if the default base is NOT 16, then I pre-scan for that suffix
* and replace all non-symbolic occurrences with an internal shift operator ('^_').
*
* Note that Str.parseInt(), which parseValue() relies on, supports both the MACRO-10 base prefix overrides
* and the binary shifting suffix ('B'), but since that suffix can also be a bracketed expression, we have to
* support it here as well.
*
* MACRO-10 supports only a subset of all the PCjs operators; for example, MACRO-10 doesn't support any of
* the boolean logical/compare operators. But unless we run into conflicts, I prefer sticking with this
* common set of operators.
*
* All whitespace in the expression is collapsed to single spaces, and space has been added to the list
* of "operators", but its sole function is as a separator, not as an operator. parseArray() will ignore
* single spaces as long as they are preceded and/or followed by a "real" operator. It would be dangerous
* to remove spaces entirely, because if an operator-less expression like "A B" was passed in, we would want
* that to generate an error; if we converted it to "AB", evaluation might inadvertently succeed.
*/
var regExp = /({|}|\|\||&&|\||\^!|\^B|\^O|\^D|\^L|\^-|~|\^_|_|&|!=|!|==|>=|>>>|>>|>|<=|<<|<|-|\+|\^\/|\/|\*|,,| )/;
if (this.nBase != 16) {
sExp = sExp.replace(/(^|[^A-Z0-9$%.])([0-9]+)B/, "$1$2^_").replace(/\s+/g, ' ');
}
var asValues = sExp.split(regExp);
value = this.parseArray(asValues, 0, asValues.length, this.nBase, aUndefined);
if (value !== undefined && fPrint) {
this.printValue(null, value);
}
}
return value;
}
/**
* parseReference(s)
*
* Returns the given string with any "{expression}" sequences replaced with the value of the expression,
* and any "[address]" references replaced with the contents of the address. Expressions are parsed BEFORE
* addresses.
*
* @this {Debugger}
* @param {string} s
* @return {string|undefined}
*/
parseReference(s)
{
var a;
var chOpen = this.achGroup[0];
var chClose = this.achGroup[1];
var chEscape = (chOpen == '(' || chOpen == '{' || chOpen == '[')? '\\' : '';
var chInnerEscape = (chOpen == '['? '\\' : '');
var reSubExp = new RegExp(chEscape + chOpen + "([^" + chInnerEscape + chOpen + chInnerEscape + chClose + "]+)" + chEscape + chClose);
while (a = s.match(reSubExp)) {
var value = this.parseExpression(a[1]);
if (value === undefined) return undefined;
var sSearch = chOpen + a[1] + chClose;
var sReplace = value != null? this.toStrBase(value) : "undefined";
/*
* Note that by default, the String replace() method only replaces the FIRST occurrence,
* and there MIGHT be more than one occurrence of the expression we just parsed, so we could
* do this instead:
*
* s = s.split(sSearch).join(sReplace);
*
* However, that's knd of an expensive (slow) solution, and it's not strictly necessary, since
* any additional identical expressions will be picked up on a subsequent iteration through this loop.
*/
s = s.replace(sSearch, sReplace);
}
if (this.achAddress.length) {
chOpen = this.achAddress[0];
chClose = this.achAddress[1];
chEscape = (chOpen == '(' || chOpen == '{' || chOpen == '[')? '\\' : '';
chInnerEscape = (chOpen == '['? '\\' : '');
reSubExp = new RegExp(chEscape + chOpen + "([^" + chInnerEscape + chOpen + chInnerEscape + chClose + "]+)" + chEscape + chClose);
while (a = s.match(reSubExp)) {
s = this.parseAddrReference(s, a[1]);
}
}
return this.parseSysVars(s);
}
/**
* parseSysVars(s)
*
* Returns the given string with any recognized "$var" replaced with its value; eg:
*
* $ops: the number of opcodes executed since the last time it was displayed (or reset)
*
* @this {Debugger}
* @param {string} s
* @return {string}
*/
parseSysVars(s)
{
var a;
while (a = s.match(/\$([a-z]+)/i)) {
var v = null;
switch(a[1].toLowerCase()) {
case "ops":
v = this.cOpcodes - this.cOpcodesStart;
break;
}
if (v == null) break;
s = s.replace(a[0], v.toString());
}
return s;
}
/**
* parseUnary(value, nUnary)
*
* nUnary is actually a small "stack" of unary operations encoded in successive pairs of bits.
* As parseExpression() encounters each unary operator, nUnary is shifted left 2 bits, and the
* new unary operator is encoded in bits 0 and 1 (0b00 is none, 0b01 is negate, 0b10 is complement,
* and 0b11 is reserved). Here, we process the bits in reverse order (hence the stack-like nature),
* ensuring that we process the unary operators associated with this value right-to-left.
*
* Since bitwise operators see only 32 bits, more than 16 unary operators cannot be supported
* using this method. We'll let parseExpression() worry about that; if it ever happens in practice,
* then we'll have to switch to a more "expensive" approach (eg, an actual array of unary operators).
*
* @this {Debugger}
* @param {number} value
* @param {number} nUnary
* @return {number}
*/
parseUnary(value, nUnary)
{
while (nUnary) {
switch(nUnary & 0o3) {
case 1:
value = -this.truncate(value);
break;
case 2:
value = this.evalXOR(value, -1); // this is easier than adding an evalNOT()...
break;
case 3:
var bit = 35; // simple left-to-right zero-bit-counting loop...
while (bit >= 0 && !this.evalAND(value, Math.pow(2, bit))) bit--;
value = 35 - bit;
break;
}
nUnary >>>= 2;
}
return value;
}
/**
* parseValue(sValue, sName, fQuiet, nUnary)
*
* @this {Debugger}
* @param {string|undefined} sValue
* @param {string|null} [sName] is the name of the value, if any
* @param {Array|undefined|boolean} [fQuiet]
* @param {number} [nUnary] (0 for none, 1 for negate, 2 for complement, 3 for leading zeros)
* @return {number|undefined} numeric value, or undefined if sValue is either undefined or invalid
*/
parseValue(sValue, sName, fQuiet, nUnary = 0)
{
var value;
var aUndefined = Array.isArray(fQuiet)? fQuiet : undefined;
if (sValue != null) {
var iReg = this.getRegIndex(sValue);
if (iReg >= 0) {
value = this.getRegValue(iReg);
} else {
value = this.getVariable(sValue);
if (value != null) {
var sUndefined = this.getVariableFixup(sValue);
if (sUndefined) {
if (aUndefined) {
aUndefined.push(sUndefined);
} else {
var valueUndefined = this.parseExpression(sUndefined, fQuiet);
if (valueUndefined !== undefined) {
value += valueUndefined;
} else {
if (!fQuiet) {
this.println("undefined " + (sName || "value") + ": " + sValue + " (" + sUndefined + ")");
}
value = undefined;
}
}
}
} else {
/*
* A feature of MACRO-10 is that any single-digit number is automatically interpreted as base-10.
*/
value = Str.parseInt(sValue, sValue.length > 1 || this.nBase > 10? this.nBase : 10);
}
}
if (value != null) {
value = this.truncate(this.parseUnary(value, nUnary));
} else {
if (!fQuiet) {
this.println("invalid " + (sName || "value") + ": " + sValue);
}
}
} else {
if (!fQuiet) {
this.println("missing " + (sName || "value"));
}
}
return value;
}
/**
* printValue(sVar, value)
*
* @this {Debugger}
* @param {string|null} sVar
* @param {number|undefined} value
* @return {boolean} true if value defined, false if not
*/
printValue(sVar, value)
{
var sValue;
var fDefined = false;
if (value !== undefined) {
fDefined = true;
if (this.nBase == 8) {
sValue = this.toStrBase(value, this.nBits, 8, 1) + " " + value + '.';
} else {
sValue = this.toStrBase(value, this.nBits, 16, 1) + " " + this.toStrBase(value, this.nBits, 8, 1) + " " + this.toStrBase(value, this.nBits, 2, this.nBits <= 32? 8 : 6) + " " + value + '.';
}
if (value >= 0x20 && value < 0x7F) {
sValue += " '" + String.fromCharCode(value) + "'";
}
}
sVar = (sVar != null? (sVar + ": ") : "");
this.println(sVar + sValue);
return fDefined;
}
/**
* resetVariables()
*
* @this {Debugger}
* @return {Object}
*/
resetVariables()
{
var a = this.aVariables;
this.aVariables = {};
return a;
}
/**
* restoreVariables(a)
*
* @this {Debugger}
* @param {Object} a (from previous resetVariables() call)
*/
restoreVariables(a)
{
this.aVariables = a;
}
/**
* printVariable(sVar)
*
* @this {Debugger}
* @param {string} [sVar]
* @return {boolean} true if all value(s) defined, false if not
*/
printVariable(sVar)
{
var cVariables = 0;
if (this.aVariables) {
if (sVar) {
return this.printValue(sVar, this.aVariables[sVar] && this.aVariables[sVar].value);
}
var aVars = Object.keys(this.aVariables);
aVars.sort();
for (var i = 0; i < aVars.length; i++) {
this.printValue(aVars[i], this.aVariables[aVars[i]].value);
cVariables++;
}
}
return cVariables > 0;
}
/**
* delVariable(sVar)
*
* @this {Debugger}
* @param {string} sVar
*/
delVariable(sVar)
{
delete this.aVariables[sVar];
}
/**
* getVariable(sVar)
*
* @this {Debugger}
* @param {string} sVar
* @return {number|undefined}
*/
getVariable(sVar)
{
if (this.aVariables[sVar]) {
return this.aVariables[sVar].value;
}
sVar = sVar.substr(0, 6);
return this.aVariables[sVar] && this.aVariables[sVar].value;
}
/**
* getVariableFixup(sVar)
*
* @this {Debugger}
* @param {string} sVar
* @return {string|undefined}
*/
getVariableFixup(sVar)
{
return this.aVariables[sVar] && this.aVariables[sVar].sUndefined;
}
/**
* isVariable(sVar)
*
* @this {Debugger}
* @param {string} sVar
* @return {boolean}
*/
isVariable(sVar)
{
return this.aVariables[sVar] !== undefined;
}
/**
* setVariable(sVar, value, sUndefined)
*
* @this {Debugger}
* @param {string} sVar
* @param {number} value
* @param {string|undefined} [sUndefined]
*/
setVariable(sVar, value, sUndefined)
{
this.aVariables[sVar] = {value, sUndefined};
}
/**
* toStrBase(n, nBits, nBase, nGrouping)
*
* Use this instead of Str's toOct()/toDec()/toHex() to convert numbers to the Debugger's default base.
*
* @this {Debugger}
* @param {number|null|undefined} n
* @param {number} [nBits] (-1 to strip leading zeros, 0 to allow a variable number of digits)
* @param {number} [nBase]
* @param {number} [nGrouping] (if nBase is 2, this is a grouping; otherwise, it's a prefix condition)
* @return {string}
*/
toStrBase(n, nBits = 0, nBase = 0, nGrouping = 0)
{
var s;
switch(nBase || this.nBase) {
case 2:
s = Str.toBin(n, nBits > 0? nBits : 0, nGrouping);
break;
case 8:
s = Str.toOct(n, nBits > 0? ((nBits + 2)/3)|0 : 0, !!nGrouping);
break;
case 10:
/*
* The multiplier is actually Math.log(2)/Math.log(10), but an approximation is more than adequate.
*/
s = Str.toDec(n, nBits > 0? Math.ceil(nBits * 0.3) : 0);
break;
case 16:
default:
s = Str.toHex(n, nBits > 0? ((nBits + 3) >> 2) : 0, !!nGrouping);
break;
}
return (nBits < 0? Str.stripLeadingZeros(s) : s);
}
}
if (DEBUGGER) {
/*
* These are our operator precedence tables. Operators toward the bottom (with higher values) have
* higher precedence. aBinOpPrecedence was our original table; we had to add aDECOpPrecedence because
* the precedence of operators in DEC's MACRO-10 expressions differ. Having separate tables also allows
* us to remove operators that shouldn't be supported, but unless some operator creates a problem,
* I prefer to keep as much commonality between the tables as possible.
*
* Missing from these tables are the (limited) set of unary operators we support (negate and complement),
* since this is only a BINARY operator precedence, not a general-purpose precedence table. Assume that
* all unary operators take precedence over all binary operators.
*/
Debugger.aBinOpPrecedence = {
'||': 5, // logical OR
'&&': 6, // logical AND
'!': 7, // bitwise OR (conflicts with logical NOT, but we never supported that)
'|': 7, // bitwise OR
'^!': 8, // bitwise XOR (added by MACRO-10 sometime between the 1972 and 1978 versions)
'&': 9, // bitwise AND
'!=': 10, // inequality
'==': 10, // equality
'>=': 11, // greater than or equal to
'>': 11, // greater than
'<=': 11, // less than or equal to
'<': 11, // less than
'>>>': 12, // unsigned bitwise right shift
'>>': 12, // bitwise right shift
'<<': 12, // bitwise left shift
'-': 13, // subtraction
'+': 13, // addition
'^/': 14, // remainder
'/': 14, // division
'*': 14, // multiplication
'_': 19, // MACRO-10 shift operator
'^_': 19, // MACRO-10 internal shift operator (converted from 'B' suffix form that MACRO-10 uses)
'{': 20, // open grouped expression (converted from achGroup[0])
'}': 20 // close grouped expression (converted from achGroup[1])
};
Debugger.aDECOpPrecedence = {
',,': 1, // high-word,,low-word
'||': 5, // logical OR
'&&': 6, // logical AND
'!=': 10, // inequality
'==': 10, // equality
'>=': 11, // greater than or equal to
'>': 11, // greater than
'<=': 11, // less than or equal to
'<': 11, // less than
'>>>': 12, // unsigned bitwise right shift
'>>': 12, // bitwise right shift
'<<': 12, // bitwise left shift
'-': 13, // subtraction
'+': 13, // addition
'^/': 14, // remainder
'/': 14, // division
'*': 14, // multiplication
'!': 15, // bitwise OR (conflicts with logical NOT, but we never supported that)
'|': 15, // bitwise OR
'^!': 15, // bitwise XOR (added by MACRO-10 sometime between the 1972 and 1978 versions)
'&': 15, // bitwise AND
'_': 19, // MACRO-10 shift operator
'^_': 19, // MACRO-10 internal shift operator (converted from 'B' suffix form that MACRO-10 uses)
'{': 20, // open grouped expression (converted from achGroup[0])
'}': 20 // close grouped expression (converted from achGroup[1])
};
/*
* Assorted constants
*/
Debugger.TWO_POW32 = Math.pow(2, 32);
} // endif DEBUGGER
/**
* @copyright http://pcjs.org/modules/pdp10/lib/debugger.js (C) Jeff Parsons 2012-2017
*/
/**
* 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.<string>|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.
*
* @this {DebuggerPDP10}
* @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.nBusWidth = 18; // default value, updated by initBus()
this.nBits = 36; // default integer precision
this.achGroup = ['<','>'];
this.achAddress = [];
/*
* Most commands that require an address call parseAddr(), and if a dbgAddr parameter is supplied
* as as well (eg, dbgAddrCode, dbgAddrData), then that address will be used as the default.
*
* For TEMPORARY breakpoint addresses, we set fTemporary to true, so that they can be automatically
* cleared when they're hit.
*/
this.dbgAddrAcc = this.newAddr();
this.dbgAddrCode = this.newAddr(0);
this.dbgAddrData = this.newAddr(0);
this.dbgAddrAssemble = this.newAddr(0);
/*
* 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'];
this.aCommands = [];
/*
* Define remaining miscellaneous DebuggerPDP10 properties.
*/
this.aOpReserved = [];
this.nStep = 0;
this.sCmdTracePrev = null;
this.sCmdDumpPrev = null;
this.nSuppressBreaks = 0;
this.cInstructions = this.cInstructionsStart = 0;
this.nCycles = this.nCyclesStart = this.msStart = 0;
this.controlDebug = null;
this.panel = null;
/**
* This records any active Macro10 assembler object.
*
* @type {Macro10|null}
*/
this.macro10 = null;
/*
* Make it easier to access DebuggerPDP10 commands from an external REPL;
* eg, the WebStorm "live" console window:
*
* 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};
}
/**
* copyAddr(dbgAddr, dbgCopy)
*
* Updates an EXISTING DbgAddrPDP10 object, initialized with specified values and/or defaults.
*
* @this {DebuggerPDP10}
* @param {DbgAddrPDP10} dbgAddr
* @param {DbgAddrPDP10} dbgCopy
* @return {DbgAddrPDP10}
*/
copyAddr(dbgAddr, dbgCopy)
{
dbgAddr.addr = dbgCopy.addr;
dbgAddr.fPhysical = dbgCopy.fPhysical;
dbgAddr.fTemporary = dbgCopy.fTemporary;
dbgAddr.nBase = dbgCopy.nBase;
return dbgAddr;
}
/**
* setAddr(dbgAddr, addr, fPhysical, nBase)
*
* Updates an EXISTING DbgAddrPDP10 object, initialized with specified values and/or defaults.
*
* @this {DebuggerPDP10}
* @param {DbgAddrPDP10} dbgAddr
* @param {number} addr
* @param {boolean} [fPhysical]
* @param {number} [nBase]
* @return {DbgAddrPDP10}
*/
setAddr(dbgAddr, addr, fPhysical, nBase)
{
dbgAddr.addr = addr;
dbgAddr.fPhysical = fPhysical || false;
dbgAddr.fTemporary = false;
dbgAddr.nBase = nBase;
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.fTemporary, dbgAddr.nBase, 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;
this.nBusWidth = bus.getWidth();
/*
* Override the Debugger's message configuration if specified.
*/
var sMessages = /** @type {string|undefined} */ (cmp.getMachineParm('messages'));
if (sMessages) this.messageInit(sMessages);
/*
* Override the Debugger's initialization commands if specified.
*/
var sCommands = /** @type {string|undefined} */ (cmp.getMachineParm('commands'));
if (sCommands) this.sInitCommands = sCommands;
/*
* 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.WORD_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);
}
}
/**
* evalMUL(dst, src)
*
* Overrides the standard multiplication function with one that honors PDP-10 semantics and precision.
*
* @this {DebuggerPDP10}
* @param {number} dst
* @param {number} src
* @return {number} (dst * src)
*/
evalMUL(dst, src)
{
/*
* The CPU requires that all 36-bit inputs/outputs be UNSIGNED, whereas our expression evaluator allows signed
* inputs/outputs. So we perform two's complement conversions on all inputs/outputs as needed.
*/
if (dst < 0) dst += PDP10.WORD_LIMIT;
if (src < 0) src += PDP10.WORD_LIMIT;
var result = PDP10.doMUL.call(this.cpu, dst, src, false, true);
if (result >= PDP10.INT_LIMIT) result -= PDP10.WORD_LIMIT;
if (MAXDEBUG) {
var resultJS = this.truncate(dst * src);
if (resultJS !== result) {
var sReference = this.macro10? (" @" + this.toStrBase(this.macro10.nLocation)) : "";
var sResults = "PDP-10: " + this.toStrBase(result, 36) + " JavaScript: " + this.toStrBase(resultJS, 36);
this.println("MUL(" + this.toStrBase(dst, 36) + "," + this.toStrBase(src, 36) + ") " + sResults + sReference);
}
}
return result;
}
/**
* parseAddr(sAddr, dbgAddr)
*
* 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 {DbgAddrPDP10} [dbgAddr]
* @return {DbgAddrPDP10}
*/
parseAddr(sAddr, dbgAddr)
{
var fPhysical, nBase;
if (!dbgAddr) dbgAddr = this.newAddr();
var addr = dbgAddr.addr;
if (sAddr !== undefined) {
sAddr = this.parseReference(sAddr);
var ch = sAddr.charAt(0);
if (ch == '%') {
fPhysical = true;
sAddr = sAddr.substr(1);
}
var dbgAddrTmp = this.findSymbolAddr(sAddr);
if (dbgAddrTmp) return dbgAddrTmp;
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);
}
if (addr != null) {
addr = this.validateWord(addr, this.nBusWidth);
this.setAddr(dbgAddr, 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]);
}
}
}
/**
* validateWord(w, bits)
*
* @this {DebuggerPDP10}
* @param {number} w
* @param {number} [bits]
* @return {number}
*/
validateWord(w, bits = 36)
{
/*
* Although it's expected that most callers will supply unsigned 36-bit values, we're nice about
* converting any signed values to their unsigned (two's complement) counterpart, provided they are
* within the acceptable range. Any values outside that range will be dealt with afterward.
*/
if (w < 0 && w >= -PDP10.INT_LIMIT) {
w += PDP10.WORD_LIMIT;
}
var value = Math.trunc(Math.abs(w)) % Math.pow(2, bits);
if (DEBUG && w !== value) {
this.println("validateWord(" + Str.toOct(w) + "): out of range, truncated to " + Str.toOct(value));
}
return value;
}
/**
* 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} default base representation of off
*/
toStrOffset(off)
{
return this.toStrBase(off, 18);
}
/**
* toStrAddr(dbgAddr)
*
* @this {DebuggerPDP10}
* @param {DbgAddrPDP10} dbgAddr
* @return {string} default base representation of the address
*/
toStrAddr(dbgAddr)
{
return this.toStrOffset(dbgAddr.addr);
}
/**
* toStrWord(w)
*
* @this {DebuggerPDP10}
* @param {number} w (up to, but not including, WORD_LIMIT)
* @return {string} octal representation of the 36-bit word, as two 18-bit values
*/
toStrWord(w)
{
/*
* ADDR_LIMIT is not derived from WORD_LIMIT; we're just taking advantage of the fact
* that ADDR_LIMIT happens to be exactly half of WORD_LIMIT, and they are both powers of two.
*/
return this.toStrBase(w / PDP10.ADDR_LIMIT, 18) + ' ' + this.toStrBase(w % PDP10.ADDR_LIMIT, 18);
}
/**
* 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, this.dbgAddrData));
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.<string>} 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.FAULT | 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.<string>)} 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}
*/
getRegName(iReg)
{
return DebuggerPDP10.REGNAMES[iReg] || "";
}
/**
* getRegValue(iReg)
*
* @this {DebuggerPDP10}
* @param {number} iReg
* @return {number|undefined}
*/
getRegValue(iReg)
{
var value;
var cpu = this.cpu;
switch(iReg) {
case DebuggerPDP10.REGS.PC:
value = cpu.getPC();
break;
case DebuggerPDP10.REGS.RA:
value = cpu.regRA;
break;
case DebuggerPDP10.REGS.EA:
value = cpu.regEA;
break;
case DebuggerPDP10.REGS.PS:
value = cpu.getPS();
break;
case DebuggerPDP10.REGS.OV:
value = (cpu.regPS & PDP10.PSFLAG.AROV)? 1 : 0;
break;
case DebuggerPDP10.REGS.C0:
value = (cpu.regPS & PDP10.PSFLAG.CRY0)? 1 : 0;
break;
case DebuggerPDP10.REGS.C1:
value = (cpu.regPS & PDP10.PSFLAG.CRY1)? 1 : 0;
break;
case DebuggerPDP10.REGS.BI:
value = (cpu.regPS & PDP10.PSFLAG.BIS)? 1 : 0;
break;
case DebuggerPDP10.REGS.ND:
value = (cpu.regPS & PDP10.PSFLAG.DCK)? 1 : 0;
break;
case DebuggerPDP10.REGS.PD:
value = (cpu.regPS & PDP10.PSFLAG.PDOV)? 1 : 0;
break;
}
return value;
}
/**
* setRegValue(iReg, value)
*
* @this {DebuggerPDP10}
* @param {number} iReg
* @param {number} value
*/
setRegValue(iReg, value)
{
var flag = 0;
var cpu = this.cpu;
switch(iReg) {
case DebuggerPDP10.REGS.PC:
cpu.setPC(value);
this.setAddr(this.dbgAddrCode, cpu.getPC());
break;
case DebuggerPDP10.REGS.PS:
cpu.setPS(value);
break;
case DebuggerPDP10.REGS.OV:
flag = PDP10.PSFLAG.AROV;
break;
case DebuggerPDP10.REGS.C0:
flag = PDP10.PSFLAG.CRY0;
break;
case DebuggerPDP10.REGS.C1:
flag = PDP10.PSFLAG.CRY1;
break;
case DebuggerPDP10.REGS.BI:
flag = PDP10.PSFLAG.BIS;
break;
case DebuggerPDP10.REGS.ND:
flag = PDP10.PSFLAG.DCK;
break;
case DebuggerPDP10.REGS.PD:
flag = PDP10.PSFLAG.PDOV;
break;
}
if (flag) {
if (value) {
cpu.regPS |= flag;
} else {
cpu.regPS &= ~flag;
}
}
}
/**
* 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, true);
}
}
/**
* 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 = true, sCmd)
{
if (!this.fInit) return;
if (sCmd) {
this.println(DebuggerPDP10.PROMPT + sCmd);
}
this.setAddr(this.dbgAddrCode, 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.setAddr(this.dbgAddrCode, 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.dbgAddrCode));
state.set(1, this.packAddr(this.dbgAddrData));
state.set(2, this.packAddr(this.dbgAddrAssemble));
state.set(3, [this.aPrevCmds, this.fAssemble, this.bitsMessage]);
state.set(4, 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[3] !== undefined) {
this.dbgAddrCode = this.unpackAddr(data[i++]);
this.dbgAddrData = 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[4]) this.aSymbolTable = data[4];
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.A_SHIFT) == 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 >= 0) {
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;
}
/**
* findInstruction(opCode, fOperands)
*
* @this {DebuggerPDP10}
* @param {number} opCode
* @param {boolean} [fOperands] (optional; default is true)
* @return {string}
*/
findInstruction(opCode, fOperands = true)
{
var opNum, opMask, aModes, iMode = 0;
var op = (opCode / PDP10.OPCODE.OP_SCALE)|0;
for (var mask in DebuggerPDP10.OPTABLE) {
var opMasks = DebuggerPDP10.OPTABLE[mask];
opNum = opMasks[op & mask];
if (opNum) {
opMask = +mask;
/*
* When we extracted op from opCode using OP_SCALE, we included 6 additional bits
* to help distinguish OPIO instructions from non-OPIO instructions. But for the
* following tests, we don't need those bits, so we get rid of them now.
*/
op >>= 6;
switch(opMask) {
case PDP10.OPCODE.OPMODE:
aModes = DebuggerPDP10.OPMODES;
iMode = (op & 3);
break;
case PDP10.OPCODE.OPCOMP:
aModes = DebuggerPDP10.OPCOMPS;
iMode = (op & 7);
break;
case PDP10.OPCODE.OPTEST:
aModes = DebuggerPDP10.OPTESTS;
iMode = ((op & 0o60) >> 2) | ((op & 0o6) >> 1);
break;
}
break;
}
}
var sMode = aModes && aModes[iMode] || "";
if (sMode == "S" && opNum > DebuggerPDP10.OPS.MOVM) sMode = "B";
var sOperation = DebuggerPDP10.OPNAMES[opNum || 0] + sMode;
if (!fOperands) {
if (!opNum) sOperation = "";
} else {
if (!opNum) {
sOperation = Str.pad(sOperation, 8) + this.toStrWord(opCode);
} else {
var n, sOperand;
if (opMask == PDP10.OPCODE.OPIO) {
n = (opCode / PDP10.OPCODE.IO_SCALE) & PDP10.OPCODE.IO_MASK;
sOperand = this.toStrBase(n, -1);
} else {
n = (opCode >> PDP10.OPCODE.A_SHIFT) & PDP10.OPCODE.A_MASK;
sOperand = this.toStrBase(n, -1);
for (var m = 0; sOperand && m < DebuggerPDP10.ALTOPS.length; m++) {
if (opNum == DebuggerPDP10.ALTOPS[m][0]) {
var opAlt = DebuggerPDP10.ALTOPS[m][n];
if (opAlt) {
sOperation = DebuggerPDP10.OPNAMES[opAlt];
sOperand = "";
break;
}
}
}
}
sOperation = Str.pad(sOperation, 8) + (sOperand? sOperand + ',' : "");
if (opCode & PDP10.OPCODE.I_FIELD) sOperation += '@';
sOperation += this.toStrBase(opCode & PDP10.OPCODE.Y_MASK, -1);
var i = (opCode >> PDP10.OPCODE.X_SHIFT) & PDP10.OPCODE.X_MASK;
if (i) sOperation += '(' + this.toStrBase(i, -1) + ')';
}
}
return sOperation;
}
/**
* 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 dbgAddrOp = this.newAddr(dbgAddr.addr);
var opCode = this.getWord(dbgAddr, 1);
var sOperation = this.findInstruction(opCode);
var sOpcodes = "";
var sLine = this.toStrAddr(dbgAddrOp) + ":";
if (dbgAddrOp.addr !== PDP10.ADDR_INVALID && dbgAddr.addr !== PDP10.ADDR_INVALID) {
do {
var w = this.getWord(dbgAddrOp, 1);
sOpcodes += ' ' + this.toStrWord(w);
if (dbgAddrOp.addr == null) break;
} while (dbgAddrOp.addr != dbgAddr.addr);
}
sLine += Str.pad(sOpcodes, 16) + sOperation;
if (sComment) {
sLine = Str.pad(sLine, 48) + ';' + (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);
}
}
return sLine;
}
/**
* parseInstruction(sOpcode, sOperands, addr, aUndefined)
*
* @this {DebuggerPDP10}
* @param {string} sOpcode
* @param {string} [sOperands]
* @param {number} [addr] of memory where this instruction is being assembled
* @param {Array} [aUndefined]
* @return {number} (opcode, or -1 if unrecognized instruction)
*/
parseInstruction(sOpcode, sOperands, addr, aUndefined)
{
var opCode = -1;
var opMask, opNum;
if (!sOpcode) {
/*
* MACRO-10 also allows instructions to be assembled without an opcode (ie, just an address expression),
* so if that's all we have, skip the opcode parsing.
*/
if (sOperands) opCode = opMask = 0;
}
else {
var sMnemonic = sOpcode.toUpperCase();
/*
* Perform any alternate mnemonic substitutions first
*/
for (var m = 0; m < DebuggerPDP10.ALTOPS.length; m++) {
for (var n in DebuggerPDP10.ALTOPS[m]) {
if (!+n) continue;
opNum = DebuggerPDP10.ALTOPS[m][n];
if (sMnemonic == DebuggerPDP10.OPNAMES[opNum]) {
sMnemonic = DebuggerPDP10.OPNAMES[DebuggerPDP10.ALTOPS[m][0]];
if (sOperands) sOperands = this.toStrBase(+n) + ',' + sOperands;
break;
}
}
}
for (var mask in DebuggerPDP10.OPTABLE) {
var aModes;
opMask = +mask;
var opMasks = DebuggerPDP10.OPTABLE[mask];
switch (opMask) {
case PDP10.OPCODE.OPMODE:
aModes = DebuggerPDP10.OPMODES;
break;
case PDP10.OPCODE.OPCOMP:
aModes = DebuggerPDP10.OPCOMPS;
break;
case PDP10.OPCODE.OPTEST:
aModes = DebuggerPDP10.OPTESTS;
break;
default:
aModes = [""];
break;
}
var opMode = 0;
for (var op in opMasks) {
opNum = opMasks[op];
for (var iMode = 0; iMode < aModes.length; iMode++) {
var sMode = aModes[iMode];
if (sMode == "S" && opNum > DebuggerPDP10.OPS.MOVM) sMode = "B";
var sCandidate = DebuggerPDP10.OPNAMES[opNum] + sMode;
if (sMnemonic == sCandidate) {
if (opMask != PDP10.OPCODE.OPTEST) {
opMode = iMode;
} else {
opMode = ((iMode & 0o3) << 1) | ((iMode & 0o14) << 2);
}
opCode = (op | (opMode << 6)) * PDP10.OPCODE.OP_SCALE;
break;
}
}
if (opCode >= 0) break;
}
if (opCode >= 0) break;
}
/*
* MACRO-10 also allows instructions to be assembled without an opcode (ie, just an address expression),
* so we'll give that a try next (as long as we're not mashing two symbols together).
*/
if (opCode < 0 && (!sOperands || !sOperands.match(/^[0-9A-Z$%.?]/i))) {
sOperands = sOpcode + sOperands;
sOpcode = "";
opCode = 0;
}
}
if (opCode >= 0) {
if (sOperands) {
var aOperands = sOperands.split(',');
if (aOperands.length > 2) {
if (!aUndefined) this.println("too many operands: " + sOperands);
aOperands.length = 0;
opCode = -1;
}
for (var i = 0; i < aOperands.length; i++) {
var sOperand = aOperands[i].trim();
if (!sOperand) continue;
var match = sOperand.match(/(@?)([^(]*)\(?([^)]*)\)?/);
if (!match) {
if (!aUndefined) this.println("unknown operand: " + sOperand);
opCode = -1;
break;
}
/*
* If the operand contains an indirection operator (@) and/or index register (X), we parse those
* first and update the indirect (I) bit and index (X) bits as appropriate. The order is important,
* because if we parse them AFTER parsing the address expression, we might lose an undefined symbol
* indication, and if the caller needs to handle address fixups, that would be bad.
*/
if (match[1]) opCode += PDP10.OPCODE.I_FIELD;
sOperand = match[3];
if (sOperand) {
operand = this.parseExpression(sOperand, aUndefined);
if (operand == undefined) {
opCode = -1;
break;
}
/*
* Here's a fun tidbit from the April 1978 MACRO-10 manual, p. 4-5:
*
* NOTE: To assemble the index, MACRO places the index register address in a fullword of storage,
* swaps its halfwords, and then adds the swapped word to the instruction word.
*
* Which means that an instruction like this (where AC is zero):
*
* 8839 037653 205 00 0 00 400000 MOVSI AC,(1B<^O<AC>>) ;INITIALIZE AC
*
* produces an instruction that does NOT use indexing at all, even though it is coded as such. So my
* simplistic masking of the index operand with PDP10.OPCODE.X_MASK, while logical, was completely wrong:
*
* if (operand < 0 || operand > PDP10.OPCODE.X_MASK) {
* operand &= PDP10.OPCODE.X_MASK;
* if (MAXDEBUG) this.println("index (" + sOperand + ") truncated to " + this.toStrBase(operand));
* }
* opCode += operand << PDP10.OPCODE.X_SHIFT;
*/
operand = PDP10.SWAP(this.truncate(operand, 36, true));
opCode += operand;
}
sOperand = match[2];
if (i || aOperands.length == 1) {
/*
* If this is NOT the first operand, replace all periods NOT preceded by a digit with the current address.
*/
if (!sOperand) {
sOperand = "0";
} else {
sOperand = sOperand.replace(/(^|[^0-9])\./g, "$1" + this.toStrOffset(addr));
}
}
var operand = this.parseExpression(sOperand, aUndefined);
if (operand == undefined) {
opCode = -1;
break;
}
if (!i && aOperands.length > 1) {
if (opMask == PDP10.OPCODE.OPIO) {
if (operand < 0 || operand > PDP10.OPCODE.IO_MASK) {
operand &= PDP10.OPCODE.IO_MASK;
if (MAXDEBUG) this.println("device code (" + sOperand + ") truncated to " + this.toStrBase(operand));
}
opCode += (operand * PDP10.OPCODE.IO_SCALE);
}
else {
if (operand < 0 || operand > PDP10.OPCODE.A_MASK) {
operand &= PDP10.OPCODE.A_MASK;
if (MAXDEBUG) this.println("accumulator (" + sOperand + ") truncated to " + this.toStrBase(operand));
}
opCode += (operand << PDP10.OPCODE.A_SHIFT);
}
continue;
}
/*
* I came across what I believe is a typo in the DEC "DAKAC" diagnostic:
*
* CAME [0,-1] ;PASS TEST IF C(AC)=0,,-1
*
* Based on the comment, it's clear what they really meant was either "[0,,-1]" or "[XWD 0,-1]".
* However, they still got the desired result, which means when the assembler parses an mnemonic-less
* instruction like "0,-1", it must truncate the second (address) operand.
*
* TODO: Determine if I should ALWAYS truncate. I'm trying to retain the flexibility of allowing
* a full 36-bit instruction to be encoded with a single numeric expression (ie, one operand).
*/
if (sOpcode || i) {
if (operand < 0 || operand > PDP10.OPCODE.Y_MASK) {
operand &= PDP10.ADDR_MASK;
if (MAXDEBUG) this.println("address (" + sOperand + ") truncated to " + this.toStrBase(operand));
}
}
opCode += operand;
}
}
//
// TODO: Complain about missing operands only if we know the instruction requires them.
//
// else {
// this.println("missing operand(s)");
// opCode = -1;
// }
}
if (opCode < 0 && !aUndefined) {
this.println("unknown instruction: " + sOpcode + ' ' + sOperands);
}
return opCode;
}
/**
* 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;
}
/**
* getAccOutput(iAcc)
*
* @this {DebuggerPDP10}
* @param {number} iAcc
* @return {string}
*/
getAccOutput(iAcc)
{
var sReg = Str.toOct(iAcc, 2);
this.setAddr(this.dbgAddrAcc, iAcc);
sReg += '=' + this.toStrBase(this.getWord(this.dbgAddrAcc), 36) + ' ';
return sReg;
}
/**
* getRegOutput(iReg)
*
* @this {DebuggerPDP10}
* @param {number} iReg
* @return {string}
*/
getRegOutput(iReg)
{
var sReg = this.getRegName(iReg);
if (sReg) {
var nBits = (iReg >= DebuggerPDP10.REGS.OV? 1 : (iReg == DebuggerPDP10.REGS.RA? 23 : 18));
sReg += '=' + this.toStrBase(this.getRegValue(iReg), nBits) + ' ';
}
return sReg;
}
/**
* getMiscDump()
*
* @this {DebuggerPDP10}
* @return {string}
*/
getMiscDump()
{
var sDump = "";
for (var i = 0; i < DebuggerPDP10.REGNAMES.length; i++) {
sDump += this.getRegOutput(i);
}
return sDump;
}
/**
* getRegDump(fMisc)
*
* For now, fMisc defaults to true, providing a full register dump by default.
*
* @this {DebuggerPDP10}
* @param {boolean|undefined} [fMisc] (true to include misc registers)
* @return {string}
*/
getRegDump(fMisc = true)
{
var sDump = "";
for (var i = 0; i < 16; i++) {
if (i && !(i & 3)) sDump += '\n';
sDump += this.getAccOutput(i);
}
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;
}
/**
* loadImage(aWords, addrStart)
*
* @this {DebuggerPDP10}
* @param {Array.<number>} aWords
* @param {number|null|undefined} addrStart
*/
loadImage(aWords, addrStart)
{
var bus = this.bus;
var dbg = this.dbg;
var nWords = 0, addrLo = null, addrHi = 0;
aWords.forEach(function(w, addr) {
bus.setWord(addr, w);
if (addrLo == null) addrLo = addr;
if (addr > addrHi) addrHi = addr;
nWords++;
});
if (!nWords) {
this.println("no data");
} else {
var sStart = "start address ";
if (addrStart != null) {
this.cpu.setPC(addrStart);
sStart += this.toStrBase(addrStart);
} else {
sStart += "unspecified";
}
this.println(nWords + " words loaded at " + this.toStrBase(addrLo) + '-' + this.toStrBase(addrHi) + ", " + sStart);
this.updateStatus();
}
}
/**
* returnSymbol(iTable, iOffset, aSymbol)
*
* Helper function for findSymbol().
*
* @this {DebuggerPDP10}
* @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 opcode mnemonic (eg, "hrli")
* [3]: the operands, if any
*
* 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 100 hrli 1,111111
* a 101 hrri 1,444444
* ...
*
* without ever entering "assemble mode", but of course, that requires more typing and doesn't take advantage
* of automatic target address advancement (see dbgAddrAssemble).
*
* When filename(s) or URL(s) are provided in lieu of an opcode, we pass those on to the Macro10 component for
* assembling (multiple files must be separated by semicolons), along with any option letters that were included
* with the "a" command; for example, if "ap" was specified, then "p" will be passed to Macro10 as an option.
*
* See the Macro10 component for a list of supported options.
*
* When assembling a file, the target address determines the initial location counter for the assembly process,
* but that can always be overridden by a LOC (or RELOC) pseudo-op in the file. The target address will also be
* used as the starting address unless that's overridden by an END pseudo-op. In the absence of a target address,
* the location counter starts at zero, and the starting address defaults to the PC register.
*
* @this {DebuggerPDP10}
* @param {Array.<string>} asArgs is the complete argument array, beginning with the "a" command in asArgs[0]
* @return {boolean}
*/
doAssemble(asArgs)
{
var sOptions = asArgs[0].substr(1);
var sAddr = asArgs[1] && asArgs[1][0] >= '0' && asArgs[1][0] <= '9'? asArgs[1] : undefined;
var sOpcode = sAddr? asArgs[2] : asArgs[1];
var dbgAddr = this.parseAddr(sAddr, this.dbgAddrAssemble);
if (!sOpcode) {
this.println("begin assemble at " + this.toStrAddr(dbgAddr));
this.fAssemble = true;
this.cmp.updateDisplays();
return true;
}
var match = sOpcode.match(/^(['"]?)(.*?)(\.klm|\.mac|\.html|\.txt|)\1$/i);
if (match && (match[1] || match[3])) {
var dbg = this;
var cpu = this.cpu;
dbgAddr = this.parseAddr(sAddr);
if (this.macro10) {
dbg.println("assembly already in progress");
}
else {
var sFile = match[2] + match[3];
if (!match[3]) sOptions += 's';
var addrLoad = dbgAddr.addr;
var macro10 = this.macro10 = new Macro10(dbg);
macro10.assembleFiles(sFile, addrLoad, sOptions, function doneMacro10(nErrorCode, sURL) {
if (!nErrorCode) {
/*
* NOTE: Most Debugger operations run in the context of doCommand(), which catches any exceptions;
* however, this callback may be running in a different context (eg, a network request callback), so
* better safe than sorry.
*/
try {
var addrStart = macro10.getStart();
if (addrStart == null) addrStart = addrLoad;
dbg.loadImage(macro10.getImage(), addrStart);
} catch(e) {
if (typeof e == "number") {
nErrorCode = e || -1;
} else {
dbg.println(e.message);
nErrorCode = -1; // fake error so that command processing stops
}
}
}
if (nErrorCode) {
dbg.println("error (" + nErrorCode + ") processing " + (sURL || sFile));
}
dbg.macro10 = null;
if (!nErrorCode) dbg.doCommands();
});
}
return false;
}
asArgs.shift();
asArgs.shift();
asArgs.shift();
var sOperands = asArgs.join("");
var opCode = this.parseInstruction(sOpcode, sOperands, dbgAddr.addr || 0);
if (opCode >= 0) {
this.setWord(dbgAddr, opCode);
this.println(this.getInstruction(dbgAddr));
}
return true;
}
/**
* 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 = sAddr == '*'? this.newAddr() : this.parseAddr(sAddr, this.dbgAddrCode);
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)
{
this.cmp.clearPanel();
}
/**
* doDump(asArgs)
*
* @this {DebuggerPDP10}
* @param {Array.<string>} 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 len = 0;
var fJSON = (sCmd == "ds");
var dbgAddr = this.parseAddr(sAddr, this.dbgAddrData);
if (sLen) {
if (sLen.charAt(0) == 'l') {
sLen = sLen.substr(1) || sBytes;
len = this.parseValue(sLen);
}
else {
var dbgAddrEnd = this.parseAddr(sLen);
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 size = (sCmd == "db"? 1 : 2);
var nWords = len || 32;
var nWordsPerLine = (size == 1? 1 : 4);
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 += w;
} else {
sData += this.toStrWord(w);
sData += ' ';
}
/*
* TODO: Provide some UI for choosing whether to dump SIXBIT or ASCII data.
*/
var nBits = 7;
var shift = 36 - nBits;
for (var i = 0; size == 1 && shift >= 0; i++) {
var c = ((w / Math.pow(2, shift)) % Math.pow(2, nBits));
sData += this.toStrBase(c, nBits) + ' ';
c += (nBits == 6? 0x20 : 0);
sChars += (c < 0x20? '.' : String.fromCharCode(c));
shift -= nBits;
}
}
if (sDump) sDump += "\n";
if (fJSON) {
sDump += sData + ",";
} else {
sDump += sAddr + ": " + sData + ((n < 0)? (' ' + sChars) : "");
}
}
if (sDump) this.println(sDump);
this.nBase = nBase;
}
/**
* doEdit(asArgs)
*
* @this {DebuggerPDP10}
* @param {Array.<string>} asArgs
*/
doEdit(asArgs)
{
var fnGet, fnSet;
var sCmd = asArgs[0];
var sAddr = asArgs[1];
if (sCmd == "e" || sCmd == "ew") {
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, this.dbgAddrData);
for (var i = 2; i < asArgs.length; i++) {
var w = this.parseExpression(asArgs[i]);
if (w === undefined) break;
w = this.validateWord(w);
this.println("changing " + this.toStrAddr(dbgAddr) + " from " + this.toStrWord(fnGet.call(this, dbgAddr)) + " to " + this.toStrWord(w));
fnSet.call(this, dbgAddr, w, 1);
}
}
/**
* 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.<string>} 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);
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.<string>} 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.<string>} asArgs
*/
doOptions(asArgs)
{
switch (asArgs[1]) {
case "base":
if (asArgs[2]) {
var nBase = +asArgs[2];
if (nBase == 2 || 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.<string>} [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 cpu = this.cpu;
var fMisc = undefined;
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 value = this.parseExpression(sValue);
if (value === undefined) return;
var iReg = this.getRegIndex(sReg);
if (iReg < 0) {
this.println("unknown register: " + sReg);
return;
}
this.setRegValue(iReg, value);
this.cmp.updateDisplays();
this.println("updated registers:");
}
}
this.println(this.getRegDump(fMisc));
if (fInstruction) {
this.setAddr(this.dbgAddrCode, cpu.getXC());
this.doUnassemble(this.toStrAddr(this.dbgAddrCode));
}
}
/**
* 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 (sAddr !== undefined) {
var dbgAddr = this.parseAddr(sAddr);
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, false);
} 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)
{
var fCallStep = true;
var nRegs = (sCmd == "p"? 0 : (sCmd == "pr"? 1 : -1));
if (sOption == '?' || nRegs < 0) {
this.println("step commands:");
this.println("\tp\tstep over instruction");
this.println("\tpr\tstep over instruction with register update");
return;
}
/*
* 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, this.dbgAddrCode);
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);
if (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;
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;
}
this.copyAddr(this.dbgAddrAssemble, dbgAddr);
this.println(this.getInstruction(dbgAddr, sComment, nSequence));
nBytes -= dbgAddr.addr - addr;
nPrinted++;
}
}
/**
* splitArgs(sCmd, sDelim)
*
* @this {DebuggerPDP10}
* @param {string} sCmd
* @param {string} [sDelim]
* @return {Array.<string>}
*/
splitArgs(sCmd, sDelim = " ")
{
var asArgs = [];
var chQuote = "";
var i = 0, iLast = 0;
while (i < sCmd.length) {
var ch = sCmd[i++];
if (chQuote) {
if (ch == chQuote) {
chQuote = "";
asArgs.push(sCmd.substr(iLast, i - iLast));
iLast = i;
}
continue;
}
if (ch == '"' || ch == "'") {
chQuote = ch;
continue;
}
if (sDelim.indexOf(ch) >= 0) {
asArgs.push(sCmd.substr(iLast, i - iLast - 1));
iLast = i;
}
}
if (iLast < i) {
asArgs.push(sCmd.substr(iLast, i - iLast));
}
asArgs[0] = asArgs[0].toLowerCase();
if (asArgs && asArgs.length) {
var s0 = asArgs[0];
var ch0 = s0.charAt(0);
for (i = 1; i < s0.length; i++) {
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 (DEBUG && sCmd == "test") {
this.doTest();
return true;
}
if (!sCmd.length || sCmd == "end") {
if (this.fAssemble) {
this.println("ended assemble at " + this.toStrAddr(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':
result = 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 " + (e.stack || e.message));
result = false;
}
return result;
}
/**
* doCommands(sCmds, fSave)
*
* This function is now written so that if any async command, such as assemble ('a'), stopped the
* flow of commands by returning false, it can call us from its callback handler with no arguments,
* and command processing should continue where it left off.
*
* @this {DebuggerPDP10}
* @param {string} [sCmds]
* @param {boolean} [fSave]
* @return {boolean} true if all commands processed, false if not
*/
doCommands(sCmds, fSave)
{
if (sCmds != null) {
this.aCommands = this.parseCommand(sCmds, fSave);
}
var sCmd;
while (sCmd = this.aCommands.shift()) {
if (!this.doCommand(sCmd)) return false;
}
return true;
}
/**
* doTest()
*
* This function exercises the disassembler by performing look-ups for all possible operation codes
* and displaying the results. It's not intended to be included in the compiled version of the Debugger
* (DEBUG only).
*
* @this {DebuggerPDP10}
*/
doTest()
{
if (MAXDEBUG) {
var ops = {}, aOpXXX = [];
var op, opXXX, opCode, sOperation;
for (op = 0o00000; op <= 0o77774; op += 4) {
opCode = op * Math.pow(2, 21);
sOperation = this.findInstruction(opCode, false);
if (!sOperation) continue;
if (ops[sOperation] === undefined) {
ops[sOperation] = op;
} else {
ops[sOperation] &= op;
}
opXXX = op >> 6;
if (!aOpXXX[opXXX]) {
aOpXXX[opXXX] = sOperation;
} else if (aOpXXX[opXXX] != sOperation) {
aOpXXX[opXXX] = "XXX";
}
}
for (sOperation in ops) {
op = ops[sOperation];
this.println(Str.pad(sOperation + ":", 8) + this.toStrWord(op * Math.pow(2, 21)));
//
// The following code leveraged the disassembler to generate opcode handlers.
//
// this.println("/**");
// this.println(" * op" + sOperation + "(" + this.toStrWord(op * Math.pow(2, 21)) + ")");
// this.println(" *");
// this.println(" * @this {CPUStatePDP10}");
// this.println(" * @param {number} opCode");
// this.println(" */");
// this.println("PDP10.op" + sOperation + " = function(opCode)");
// this.println("{");
// this.println(" this.opUndefined(op);");
// this.println("};\n");
}
//
// The following code generated an opcode dispatch table.
//
// this.println("PDP10.aOpXXX = [");
// for (opXXX = 0o000; opXXX <= 0o777; opXXX++) {
// sOperation = aOpXXX[opXXX];
// sOperation = sOperation? (" PDP10.op" + sOperation + ",") : " PDP10.opUndefined,";
// sOperation = Str.pad(sOperation, 32);
// sOperation += "// " + Str.toOct(opXXX, 3, true) + "xxx";
// this.println(sOperation);
// }
// this.println("];");
}
}
/**
* 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,
HLL: 1, HLLZ: 2, HLLO: 3, HLLE: 4,
HRL: 5, HRLZ: 6, HRLO: 7, HRLE: 8,
HRR: 9, HRRZ: 10, HRRO: 11, HRRE: 12,
HLR: 13, HLRZ: 14, HLRO: 15, HLRE: 16,
MOVE: 17, MOVS: 18, MOVN: 19, MOVM: 20,
EXCH: 21, BLT: 22, PUSH: 23, POP: 24,
LDB: 25, DPB: 26, IBP: 27, ILDB: 28,
IDPB: 29, SETZ: 30, SETO: 31, SETA: 32,
SETCA: 33, SETM: 34, SETCM: 35, AND: 36,
ANDCA: 37, ANDCM: 38, ANDCB: 39, IOR: 40,
ORCA: 41, ORCM: 42, ORCB: 43, XOR: 44,
EQV: 45, LSH: 46, LSHC: 47, ROT: 48,
ROTC: 49, ADD: 50, SUB: 51, MUL: 52,
IMUL: 53, DIV: 54, IDIV: 55, ASH: 56,
ASHC: 57, FSC: 58, FADR: 59, FSBR: 60,
FMPR: 61, FDVR: 62, DFN: 63, UFA: 64,
FAD: 65, FSB: 66, FMP: 67, FDV: 68,
AOBJP: 69, AOBJN: 70, CAI: 71, CAM: 72,
JUMP: 73, SKIP: 74, AOJ: 75, AOS: 76,
SOJ: 77, SOS: 78, TR: 79, TL: 80,
TD: 81, TS: 82, XCT: 83, JFFO: 84,
JFCL: 85, JSR: 86, JSP: 87, JRST: 88,
JSA: 89, JRA: 90, PUSHJ: 91, POPJ: 92,
BLKI: 93, DATAI: 94, BLKO: 95, DATAO: 96,
CONO: 97, CONI: 98, CONSZ: 99, CONSO: 100,
UUO: 101, JOV: 102, JCRY0: 103, JCRY1: 104,
JCRY: 105, JFOV: 106, HALT: 107, JRSTF: 108,
JEN: 109
};
/*
* CPU opcode names, indexed by CPU opcode ordinal (above)
*/
DebuggerPDP10.OPNAMES = [
".WORD",
"HLL", "HLLZ", "HLLO", "HLLE",
"HRL", "HRLZ", "HRLO", "HRLE",
"HRR", "HRRZ", "HRRO", "HRRE",
"HLR", "HLRZ", "HLRO", "HLRE",
"MOVE", "MOVS", "MOVN", "MOVM",
"EXCH", "BLT", "PUSH", "POP",
"LDB", "DPB", "IBP", "ILDB",
"IDPB", "SETZ", "SETO", "SETA",
"SETCA", "SETM", "SETCM", "AND",
"ANDCA", "ANDCM", "ANDCB", "IOR",
"ORCA", "ORCM", "ORCB", "XOR",
"EQV", "LSH", "LSHC", "ROT",
"ROTC", "ADD", "SUB", "MUL",
"IMUL", "DIV", "IDIV", "ASH",
"ASHC", "FSC", "FADR", "FSBR",
"FMPR", "FDVR", "DFN", "UFA",
"FAD", "FSB", "FMP", "FDV",
"AOBJP", "AOBJN", "CAI", "CAM",
"JUMP", "SKIP", "AOJ", "AOS",
"SOJ", "SOS", "TR", "TL",
"TD", "TS", "XCT", "JFFO",
"JFCL", "JSR", "JSP", "JRST",
"JSA", "JRA", "PUSHJ", "POPJ",
"BLKI", "DATAI", "BLKO", "DATAO",
"CONO", "CONI", "CONSZ", "CONSO",
"UUO", "JOV", "JCRY0", "JCRY1",
"JCRY", "JFOV", "HALT", 'JRSTF',
"JEN"
];
DebuggerPDP10.REGS = {
PC: 0,
RA: 1,
EA: 2,
PS: 3,
OV: 4, // single-bit "register" representing the Overflow flag
C0: 5, // single-bit "register" representing the Carry 0 flag
C1: 6, // single-bit "register" representing the Carry 1 flag
BI: 7, // single-bit "register" representing the Byte Interrupt flag
ND: 8, // single-bit "register" representing the No Divide flag
PD: 9, // single-bit "register" representing the Pushdown Overflow flag
};
DebuggerPDP10.REGNAMES = [
"PC", "RA", "EA", "PS", "OV", "C0", "C1", "BI", "ND", "PD"
];
/*
* OPTABLE is a collection of masks, and each mask refers to a collection of opcode
* patterns associated with that mask; the disassembler applies each mask to the opcode,
* and when a masked opcode matches one of the associated patterns, the corresponding
* instruction is considered a match.
*/
DebuggerPDP10.OPTABLE = {
[PDP10.OPCODE.OPUUO]: { // 0o70000
0o00000: DebuggerPDP10.OPS.UUO
},
[PDP10.OPCODE.OPMASK]: { // 0o77700
0o13000: DebuggerPDP10.OPS.UFA,
0o13100: DebuggerPDP10.OPS.DFN,
0o13200: DebuggerPDP10.OPS.FSC,
0o13300: DebuggerPDP10.OPS.IBP,
0o13400: DebuggerPDP10.OPS.ILDB,
0o13500: DebuggerPDP10.OPS.LDB,
0o13600: DebuggerPDP10.OPS.IDPB,
0o13700: DebuggerPDP10.OPS.DPB,
0o24000: DebuggerPDP10.OPS.ASH,
0o24100: DebuggerPDP10.OPS.ROT,
0o24200: DebuggerPDP10.OPS.LSH,
0o24300: DebuggerPDP10.OPS.JFFO,
0o24400: DebuggerPDP10.OPS.ASHC,
0o24500: DebuggerPDP10.OPS.ROTC,
0o24600: DebuggerPDP10.OPS.LSHC,
0o25000: DebuggerPDP10.OPS.EXCH,
0o25100: DebuggerPDP10.OPS.BLT,
0o25200: DebuggerPDP10.OPS.AOBJP,
0o25300: DebuggerPDP10.OPS.AOBJN,
0o25400: DebuggerPDP10.OPS.JRST, // includes HALT, JRSTF, and JEN
0o25500: DebuggerPDP10.OPS.JFCL, // includes JOV, JCRY0, JCRY1, JCRY, and JFOV
0o25600: DebuggerPDP10.OPS.XCT,
0o26000: DebuggerPDP10.OPS.PUSHJ,
0o26100: DebuggerPDP10.OPS.PUSH,
0o26200: DebuggerPDP10.OPS.POP,
0o26300: DebuggerPDP10.OPS.POPJ,
0o26400: DebuggerPDP10.OPS.JSR,
0o26500: DebuggerPDP10.OPS.JSP,
0o26600: DebuggerPDP10.OPS.JSA,
0o26700: DebuggerPDP10.OPS.JRA,
},
[PDP10.OPCODE.OPMODE]: { // 0o77400
0o14000: DebuggerPDP10.OPS.FAD,
0o14400: DebuggerPDP10.OPS.FADR,
0o15000: DebuggerPDP10.OPS.FSB,
0o15400: DebuggerPDP10.OPS.FSBR,
0o16000: DebuggerPDP10.OPS.FMP,
0o16400: DebuggerPDP10.OPS.FMPR,
0o17000: DebuggerPDP10.OPS.FDV,
0o17400: DebuggerPDP10.OPS.FDVR,
0o20000: DebuggerPDP10.OPS.MOVE,
0o20400: DebuggerPDP10.OPS.MOVS,
0o21000: DebuggerPDP10.OPS.MOVN,
0o21400: DebuggerPDP10.OPS.MOVM,
0o22000: DebuggerPDP10.OPS.IMUL,
0o22400: DebuggerPDP10.OPS.MUL,
0o23000: DebuggerPDP10.OPS.IDIV,
0o23400: DebuggerPDP10.OPS.DIV,
0o27000: DebuggerPDP10.OPS.ADD,
0o27400: DebuggerPDP10.OPS.SUB,
0o40000: DebuggerPDP10.OPS.SETZ, // MACRO alias: CLEAR
0o40400: DebuggerPDP10.OPS.AND,
0o41000: DebuggerPDP10.OPS.ANDCA,
0o41400: DebuggerPDP10.OPS.SETM,
0o42000: DebuggerPDP10.OPS.ANDCM,
0o42400: DebuggerPDP10.OPS.SETA,
0o43000: DebuggerPDP10.OPS.XOR,
0o43400: DebuggerPDP10.OPS.IOR, // MACRO alias: OR
0o44000: DebuggerPDP10.OPS.ANDCB,
0o44400: DebuggerPDP10.OPS.EQV,
0o45000: DebuggerPDP10.OPS.SETCA,
0o45400: DebuggerPDP10.OPS.ORCA,
0o46000: DebuggerPDP10.OPS.SETCM,
0o46400: DebuggerPDP10.OPS.ORCM,
0o47000: DebuggerPDP10.OPS.ORCB,
0o47400: DebuggerPDP10.OPS.SETO,
0o50000: DebuggerPDP10.OPS.HLL,
0o50400: DebuggerPDP10.OPS.HRL,
0o51000: DebuggerPDP10.OPS.HLLZ,
0o51400: DebuggerPDP10.OPS.HRLZ,
0o52000: DebuggerPDP10.OPS.HLLO,
0o52400: DebuggerPDP10.OPS.HRLO,
0o53000: DebuggerPDP10.OPS.HLLE,
0o53400: DebuggerPDP10.OPS.HRLE,
0o54000: DebuggerPDP10.OPS.HRR,
0o54400: DebuggerPDP10.OPS.HLR,
0o55000: DebuggerPDP10.OPS.HRRZ,
0o55400: DebuggerPDP10.OPS.HLRZ,
0o56000: DebuggerPDP10.OPS.HRRO,
0o56400: DebuggerPDP10.OPS.HLRO,
0o57000: DebuggerPDP10.OPS.HRRE,
0o57400: DebuggerPDP10.OPS.HLRE
},
[PDP10.OPCODE.OPCOMP]: { // 0o77000
0o30000: DebuggerPDP10.OPS.CAI,
0o31000: DebuggerPDP10.OPS.CAM,
0o32000: DebuggerPDP10.OPS.JUMP,
0o33000: DebuggerPDP10.OPS.SKIP,
0o34000: DebuggerPDP10.OPS.AOJ,
0o35000: DebuggerPDP10.OPS.AOS,
0o36000: DebuggerPDP10.OPS.SOJ,
0o37000: DebuggerPDP10.OPS.SOS,
},
[PDP10.OPCODE.OPTEST]: { // 0o71100
0o60000: DebuggerPDP10.OPS.TR,
0o60100: DebuggerPDP10.OPS.TL,
0o61000: DebuggerPDP10.OPS.TD,
0o61100: DebuggerPDP10.OPS.TS,
},
[PDP10.OPCODE.OPIO]: { // 0o70034
0o70000: DebuggerPDP10.OPS.BLKI,
0o70004: DebuggerPDP10.OPS.DATAI,
0o70010: DebuggerPDP10.OPS.BLKO,
0o70014: DebuggerPDP10.OPS.DATAO,
0o70020: DebuggerPDP10.OPS.CONO,
0o70024: DebuggerPDP10.OPS.CONI,
0o70030: DebuggerPDP10.OPS.CONSZ,
0o70034: DebuggerPDP10.OPS.CONSO
}
};
DebuggerPDP10.OPMODES = ["", "I", "M", "S"];
DebuggerPDP10.OPCOMPS = ["", "L", "E", "LE", "A", "GE", "N", "G"];
DebuggerPDP10.OPTESTS = ["N", "NE", "NA", "NN", "Z", "ZE", "ZA", "ZN", "C", "CE", "CA", "CN", "O", "OE", "OA", "ON"];
/*
* Apparently, DEC's MACRO program permits "JFCL xxx" (with a single argument) as an alternate for "JFCL 0,xxx"
* (which in turn is long-hand for "No-op", since JFCL with 0 does nothing).
*/
DebuggerPDP10.JFCL = {
0o00: DebuggerPDP10.OPS.JFCL,
0o10: DebuggerPDP10.OPS.JOV,
0o04: DebuggerPDP10.OPS.JCRY0,
0o02: DebuggerPDP10.OPS.JCRY1,
0o06: DebuggerPDP10.OPS.JCRY,
0o01: DebuggerPDP10.OPS.JFOV
};
DebuggerPDP10.JRST = {
0o00: DebuggerPDP10.OPS.JRST,
0o04: DebuggerPDP10.OPS.HALT,
0o02: DebuggerPDP10.OPS.JRSTF,
0o12: DebuggerPDP10.OPS.JEN
};
DebuggerPDP10.ALTOPS = [
DebuggerPDP10.JFCL, DebuggerPDP10.JRST
];
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
/**
* @copyright http://pcjs.org/modules/pdp10/lib/macro10.js (C) Jeff Parsons 2012-2017
*/
/**
* Elements of tblMacros.
*
* @typedef {{
* name:(string),
* nOperand:(number),
* aParms:(Array.<string>),
* aDefaults:(Array.<string>),
* aValues:(Array.<string>),
* sText:(string),
* nLine:(number)
* }}
*/
var Mac;
/**
* Elements of tblSymbols.
*
* @typedef {{
* name:(string),
* value:(number),
* nType:(number),
* nLine:(number)
* }}
*/
var Sym;
/**
* Elements of aLiterals.
*
* @typedef {{
* name:(string),
* aWords:(Array.<number>),
* aFixups:(Array.<string>)
* }}
*/
var Lit;
/**
* Elements of stackScopes.
*
* @typedef {{
* name:(string|undefined),
* aWords:(Array.<number>),
* aFixups:(Array.<string>),
* nLocation:(number),
* nLocationScope:(number),
* nLine:(number)
* }}
*/
var Scope;
/**
* @class Macro10
* @property {string} sURL
* @property {number|null} addrLoad
* @property {string} sOptions
* @property {DebuggerPDP10} dbg
* @property {function(...)|undefined} done
* @property {number} iURL
* @property {Array.<string>} aURLs
* @property {Array.<number>} anLines
* @property {Array.<string>} asLines
* @property {number|null|undefined} addrStart
* @unrestricted
*/
class Macro10 {
/**
* Macro10(dbg)
*
* A "mini" version of DEC's MACRO-10 assembler, with just enough features to support the handful
* of DEC diagnostic source code files that we choose to throw at it.
*
* We rely on the calling component (dbg) to provide a variety of helper services (eg, println(),
* parseExpression(), etc). This is NOT a subclass of Component, so Component services are not part
* of this class.
*
* @this {Macro10}
* @param {DebuggerPDP10} dbg (used to provide helper services to the Macro10 class)
*/
constructor(dbg)
{
this.dbg = dbg;
}
/**
* init(addrLoad, sOptions, done)
*
* Initializes all instance properties. Called by assembleFiles() and assembleString().
*
* Supported options include:
*
* 'd': leave all symbols in the debugger's variable table
* 'p': print the preprocessed resource(s) without assembling them
* 'l': print lines as they are parsed
* 's': treat the URL as a string and assemble it (assembleFiles() only)
*
* The done() callback is called after the resource(s) have been loaded (if necessary), parsed, and
* assembled. The caller must use other methods to obtain further results (eg, getImage(), getStart()).
*
* The callback includes a non-zero error code if there was an error (and the URL):
*
* done(nErrorCode, sURL)
*
* @this {Macro10}
* @param {number|null} [addrLoad] (the absolute address to assemble the code at, if any)
* @param {string|undefined} [sOptions] (zero or more letter codes to control the assembly process)
* @param {function(...)|undefined} [done]
*/
init(addrLoad, sOptions, done)
{
this.addrLoad = addrLoad;
this.sOptions = (sOptions || "").toLowerCase();
this.done = done;
this.iURL = 0;
this.aURLs = [];
/**
* Number of lines per file.
*
* @type {Array.<number>}
*/
this.anLines = [];
/**
* Lines from all the resources.
*
* @type {Array.<string>}
*/
this.asLines = [];
if (MAXDEBUG) this.println("starting PCjs MACRO-10 Mini-Assembler...");
/*
* Initialize all the tables and other data structures that MACRO-10 uses.
*
* The Macros (tblMacros) and Symbols (tblSymbols) tables are fairly straightforward: they are
* indexed by a macro or symbol name, and each element is a Mac or Sym object, respectively.
*
* We also treat REPEAT blocks and CONDITIONAL (eg, IFE) blocks like macros, except that they are
* anonymous, parameter-less, and immediately invoked. REPEAT blocks are always immediately invoked
* (repeatedly, based on the repeat count), whereas CONDITIONAL blocks are either immediately
* invoked if the associated condition is true or skipped if the condition is false.
*
* Finally, we have LITERAL blocks, which are semi-anonymous (we give each one an auto-generated
* name based on the current location) and are automatically but not immediately invoked. Instead,
* after we've finished processing all the lines in the original input file, we run through all
* the LITERAL blocks in tblMacros and process the associated statement(s) at that time.
*
* Macros have the name that was assigned to them, REPEAT and conditional blocks have generated names
* that match the pseudo-op (eg, "?REPEAT", "?IFE"), and LITERAL blocks have generated location-based
* names. All generated names use a leading question mark ('?') so that they don't conflict with
* normal MACRO-10 symbols.
*/
/**
* @type {Object.<Mac>}
*/
this.tblMacros = {};
/**
* @type {Object.<Sym>}
*/
this.tblSymbols = {};
/**
* @type {Array.<Lit>}
*/
this.aLiterals = []; // array of literals
/**
* This keeps track of symbols suffixed with '#', which are later assembled into a variable pool,
* following the literal pool.
*
* @type {Array.<string>}
*/
this.aVariables = [];
/**
* @type {Array.<number>}
*/
this.aWords = []; // filled in by the various genXXX() functions
/**
* This sparse array is indexed by location, and each used entry contains any undefined symbols that
* must be evaluated to fully resolve the word at the corresponding location. NOTE: There's no requirement
* that the array be sparse; we could certainly fill each unused entry with null (ie, for locations that
* don't require a fixup).
*
* @type {Array.<string>}
*/
this.aFixups = [];
/**
* This array parallels aFixups, providing context (ie, line numbers) for any fixups that we want to analyze.
*
* @type {Array.<number>}
*/
this.aLineRefs = [];
this.nLine = 0;
this.nError = 0;
this.nLiteral = 0; // used to uniquely number literals
/**
* @type {number}
*/
this.nLocation = this.addrLoad || 0;
/**
* @type {number}
*/
this.nLocationScope = -1;
/**
* @type {Array.<Scope>}
*/
this.stackScopes = [];
this.sOperator = null; // the active operator, if any
this.nMacroDef = 0; // the active macro definition state
this.sMacroDef = null; // the active macro definition name
this.chMacroOpen = this.chMacroClose = '';
/*
* This regular expression breaks each MACRO-10 line into the following elements:
*
* [1]: label (with trailing semicolon), if any
* [2]: operator (eg, opcode mnemonic or pseudo-op), if any
* [3]: operator/operand whitespace separator, if any
* [4]: operand(s), if any
* [5]: comment, if any
*/
this.reLine = /^[ \t]*([A-Z$%.?][0-9A-Z$%.]*:|)[ \t]*([A-Z$%.][0-9A-Z$%.]*|)([ \t]*)([^;]+|)(;?[\s\S]*)/i;
this.macroCall = null; // the active macro being called, if any
/**
* If an ASCII/ASCIZ/SIXBIT pseudo-op is active, chASCII is set to the separator
* and sASCII collects the intervening character(s).
*
* @type {null|string}
*/
this.chASCII = null;
/**
* @type {string}
*/
this.sASCII = "";
this.addrStart = null;
}
/**
* assembleFiles(sURL, addrLoad, sOptions, done)
*
* Requests the resource(s) specified by sURL; multiple resources can be requested by separating
* them with semicolons. The resources are requested and combined in the same order they are listed,
* and after the last resource has been received, they are assembled as a single unit.
*
* As a courtesy to the Debugger's doAssemble() function, we allow it to select between assembleFiles()
* and assembleString() by specifying an 's' option here, rather than having two separate code paths.
*
* @this {Macro10}
* @param {string} sURL (the URL(s) of the resource to be assembled)
* @param {number|null} [addrLoad] (the absolute address to assemble the code at, if any)
* @param {string|undefined} [sOptions] (zero or more letter codes to control the assembly process)
* @param {function(...)|undefined} [done]
*/
assembleFiles(sURL, addrLoad, sOptions, done)
{
if (sOptions && sOptions.indexOf('s') >= 0) {
this.assembleString(sURL, addrLoad, sOptions, done);
return;
}
this.init(addrLoad, sOptions, done);
this.aURLs = sURL.split(';');
this.loadNextResource();
}
/**
* assembleString(sText, addrLoad, sOptions, done)
*
* Assembles the given text.
*
* @this {Macro10}
* @param {string} sText
* @param {number|null} [addrLoad] (the absolute address to assemble the code at, if any)
* @param {string|undefined} [sOptions] (zero or more letter codes to control the assembly process)
* @param {function(...)|undefined} [done]
* @return {number}
*/
assembleString(sText, addrLoad, sOptions, done)
{
this.init(addrLoad, sOptions, done);
this.asLines = sText.split(/(\r?\n)/);
this.anLines.push(this.asLines.length);
this.parseResources();
if (this.done) this.done(this.nError);
return this.nError;
}
/**
* loadNextResource()
*
* @this {Macro10}
*/
loadNextResource()
{
if (this.iURL == this.aURLs.length) {
this.parseResources();
if (this.done) this.done(this.nError);
return;
}
var macro10 = this;
var sURL = this.aURLs[this.iURL];
this.println("loading " + Str.getBaseName(sURL));
/*
* We know that local resources ending with ".MAC" are actually stored with a ".txt" extension.
*/
if (sURL.indexOf(':') < 0) {
var sExt = sURL.slice(-4).toUpperCase();
if (".MAC.KLM".indexOf(sExt) >= 0) sURL += ".txt";
}
Web.getResource(sURL, null, true, function processMacro10(sFile, sResource, nErrorCode) {
if (nErrorCode) {
if (macro10.done) macro10.done(nErrorCode, sFile);
return;
}
var sText = sResource;
if (Str.endsWith(sFile, ".html")) {
/*
* We want to parse ONLY the text between <PRE>...</PRE> tags, and eliminate any HTML entities.
*/
sText = "";
var match, re = /<pre>([\s\S]*?)<\/pre>/gi;
while (match = re.exec(sResource)) {
var s = match[1];
if (s.indexOf('&') >= 0) s = s.replace(/&lt;/gi, '<').replace(/&gt;/gi, '>').replace(/&amp;/gi, '&');
sText += s;
}
match = sText.match(/&[a-z]+;/i);
if (match) macro10.warning("unrecognized HTML entity '" + match[0] + "'");
}
/*
* For a file containing N lines, split() will return an array of N*2+1 entries, with every even entry
* containing the characters, if any, preceding a line-ending, and every odd entry (including the final
* entry) containing a line-ending.
*
* If, for some reason, split() doesn't do that, then we try to warn and patch things up as best we can.
*/
var asLines = sText.split(/(\r?\n)/);
if (asLines.length & 1) {
s = asLines.pop();
if (s) {
macro10.warning("unexpected line '" + s + "'");
asLines.push(s);
asLines.push("");
}
} else {
macro10.warning("unexpected number of lines (" + asLines.length + ")");
}
macro10.asLines = macro10.asLines.concat(asLines);
macro10.anLines[macro10.iURL] = (asLines.length >> 1);
macro10.iURL++;
setTimeout(function() {
macro10.loadNextResource();
}, 0);
});
}
/**
* getImage()
*
* Service for the Debugger to obtain the assembled data after a (hopefully) successful assembly process.
*
* @this {Macro10}
* @return {Array.<number>}
*/
getImage()
{
return this.aWords;
}
/**
* getStart()
*
* Service for the Debugger to obtain the starting address after a (hopefully) successful assembly process.
*
* @this {Macro10}
* @return {number|null|undefined}
*/
getStart()
{
return this.addrStart;
}
/**
* parseResources()
*
* Begin the assembly process.
*
* @this {Macro10}
* @return {number}
*/
parseResources()
{
var macro10 = this;
/*
* If the "preprocess" option is set, then print everything without assembling.
*/
if (this.sOptions.indexOf('p') >= 0) {
this.println(this.asLines.join(""));
return 0;
}
var a = this.dbg.resetVariables();
/*
* Add predefined device codes
*/
this.addSymbol("APR", 0); // Priority Interrupt
this.addSymbol("PI", 4); // Arithmetic Processor
try {
for (let i = 0; i < this.asLines.length; i += 2) {
this.nLine++;
/*
* If the "line" option is set, then print all the lines as they are parsed.
*/
if (this.sOptions.indexOf('l') >= 0) {
this.println(this.getLineRef() + ": " + this.asLines[i]);
}
/*
* Since, at this early stage, I'm not sure whether all the resources I'm interested in
* assembling have had their original CR/LF line endings preserved (eg, some files may have
* been converted to LF-only line endings), I'm going to skip over whatever line endings
* are in the array (stored in every OTHER entry) and insert my own uniform CR/LF sequences.
*/
if (!this.parseLine(this.asLines[i] + '\r\n')) break;
/*
* When an END statement is encountered, addrStart will change from null to either undefined
* or a starting address.
*/
if (this.addrStart !== null) break;
}
if (this.nMacroDef) {
this.error("open block", this.tblMacros[this.sMacroDef].nLine);
}
if (this.stackScopes.length) {
this.error("open scope", this.stackScopes[0].nLine);
}
/*
* Process all literals next.
*/
this.doLiterals();
/*
* Process all variables next.
*/
this.doVariables();
/*
* And last but not least, perform all fixups.
*/
this.aFixups.forEach(function processFixup(sValue, nLocation) {
let value = macro10.parseExpression(sValue, undefined, nLocation, macro10.aLineRefs[nLocation]);
if (value === undefined) return;
value += macro10.aWords[nLocation];
macro10.aWords[nLocation] = macro10.truncate(value, nLocation);
});
} catch(err) {
this.println(err.message);
this.nError = -1;
}
if (this.sOptions.indexOf('d') < 0) this.dbg.restoreVariables(a);
return this.nError;
}
/**
* parseLine(sLine, aParms, aValues, aDefaults)
*
* @this {Macro10}
* @param {string} sLine (line contents)
* @param {Array.<string>} [aParms]
* @param {Array.<string>} [aValues]
* @param {Array.<string>} [aDefaults]
* @return {boolean}
*/
parseLine(sLine, aParms, aValues, aDefaults)
{
var i, matchLine;
if (this.chASCII != null) {
sLine = this.defASCII(sLine);
}
var fParse = true;
var sLabel, sOperator = "", sSeparator, sOperands, sRemainder;
while (fParse) {
matchLine = sLine.match(this.reLine);
if (!matchLine || matchLine[5] && matchLine[5].slice(0, 1) != ';') {
this.error("failed to parse line '" + sLine + "'");
return false;
}
fParse = false;
sOperator = matchLine[2].toUpperCase();
/*
* TODO: The following kludge needs to be fixed at some point. The goal here is to prevent any
* of the caller's parameters from replacing any IRP/IRPC call parameters, but the goal is actually
* much bigger than that, because it's not just IRP/IRPC call parameters that must be left intact,
* but ANY macro call parameters; IRP/IRPC macros are just easier to pick out. Unfortunately, as
* the code is currently structured, we won't know if we're dealing with any macro call parameters
* on this line until parseMacro() is called, below.
*/
if (sOperator == Macro10.PSEUDO_OP.IRP || sOperator == Macro10.PSEUDO_OP.IRPC) {
aParms = null;
}
if (aParms) {
for (var iParm = 0; iParm < aParms.length; iParm++) {
var sParm = aParms[iParm];
var macroDef = this.tblMacros[this.sMacroDef];
if (macroDef && macroDef.aParms.indexOf(sParm) >= 0) continue;
var sReplace = aValues[iParm] || aDefaults[iParm] || "";
var iSearch = 0;
var iLimit = sLine.length - matchLine[5].length; // set the limit at the start of the comment, if any
while (iSearch < iLimit) {
var iMatch = sLine.indexOf(sParm, iSearch);
if (iMatch < 0) break;
iSearch = iMatch + 1;
var iMatchEnd = iMatch + sParm.length;
var chPre = '', chPost = '';
if ((!iMatch || !this.isSymbolChar(chPre = sLine[iMatch - 1])) && (iMatchEnd >= sLine.length || !this.isSymbolChar(chPost = sLine[iMatchEnd]))) {
/*
* If the "concatenation character" (') appears before (or after) the symbol being replaced, remove it.
*/
if (chPre == "'") iMatch--;
if (chPost == "'") iMatchEnd++;
sLine = sLine.substr(0, iMatch) + sReplace + sLine.substr(iMatchEnd);
iSearch = iMatch + sReplace.length;
fParse = true;
}
}
}
aParms = null;
}
if (this.nMacroDef) {
if (this.nMacroDef == 1) {
i = sLine.indexOf(this.chMacroOpen);
if (i >= 0) {
this.nMacroDef++;
sLine = sLine.substr(i+1);
} else {
this.error("expected " + this.sOperator + " definition in '" + sLine + "'");
}
}
if (this.nMacroDef > 1) {
sLine = this.appendMacro(sLine);
fParse = true;
}
if (this.nMacroDef) return true;
}
}
sLabel = matchLine[1];
sSeparator = matchLine[3];
sOperands = matchLine[4].trim();
sRemainder = matchLine[4] + matchLine[5];
if (sLabel) {
sLabel = sLabel.slice(0, -1);
this.addSymbol(sLabel, this.nLocation, Macro10.SYMTYPE.LABEL);
}
var matchOp;
if (sOperator && (matchOp = sOperands.match(/^([=:]+)(.*)/))) {
var nType = 0;
sLabel = sOperator;
sOperator = matchOp[1];
sOperands = matchOp[2];
if (sOperator == '==') {
nType |= Macro10.SYMTYPE.PRIVATE;
}
else if (sOperator == '=:') {
nType |= Macro10.SYMTYPE.INTERNAL;
}
this.addSymbol(sLabel, sOperands, nType);
sOperator = sOperands = "";
}
if (!sOperator && !sOperands) return true;
this.sOperator = sOperator;
/*
* Check the operands for a literal. If the line contains and/or ends with a literal
* we record it and replace it with an internal symbol. We assume only one literal per line,
* especially since they can be open-ended (ie, continue for multiple lines).
*/
var sLiteral = this.getLiteral(sOperands);
if (sLiteral) {
sOperands = sOperands.replace(sLiteral, this.defMacro(Macro10.PSEUDO_OP.LITERAL, this.getLiteral(sRemainder)));
if (!sSeparator) sSeparator = "\t";
}
/*
* Check the operands for any reserved symbols (ie, symbols with a trailing '#', such as "USER#").
*/
var sSymbol;
while (sSymbol = this.getReserved(sOperands)) {
sOperands = sOperands.replace(sSymbol, sSymbol.slice(0, -1));
}
if (!this.parseMacro(sOperator, sOperands)) {
switch (sOperator) {
case Macro10.PSEUDO_OP.ASCII:
case Macro10.PSEUDO_OP.ASCIZ:
case Macro10.PSEUDO_OP.SIXBIT:
this.defASCII(sRemainder);
break;
case Macro10.PSEUDO_OP.BLOCK:
this.defBLOCK(sOperands);
break;
case Macro10.PSEUDO_OP.BYTE:
this.defBYTE(sOperands);
break;
case Macro10.PSEUDO_OP.END:
this.defEND(sOperands);
break;
case Macro10.PSEUDO_OP.EXP:
this.defWord(sOperands);
break;
case Macro10.PSEUDO_OP.LIT:
this.doLiterals();
break;
case Macro10.PSEUDO_OP.LOC:
this.defLocation(sOperands);
break;
case Macro10.PSEUDO_OP.VAR:
this.doVariables();
break;
case Macro10.PSEUDO_OP.XWD:
this.defXWD(sOperands);
break;
case Macro10.PSEUDO_OP.DEFINE:
case Macro10.PSEUDO_OP.IF1:
case Macro10.PSEUDO_OP.IFDEF:
case Macro10.PSEUDO_OP.IFDIF:
case Macro10.PSEUDO_OP.IFE:
case Macro10.PSEUDO_OP.IFG:
case Macro10.PSEUDO_OP.IFGE:
case Macro10.PSEUDO_OP.IFIDN:
case Macro10.PSEUDO_OP.IFL:
case Macro10.PSEUDO_OP.IFLE:
case Macro10.PSEUDO_OP.IFN:
case Macro10.PSEUDO_OP.IFNDEF:
case Macro10.PSEUDO_OP.IRP:
case Macro10.PSEUDO_OP.IRPC:
case Macro10.PSEUDO_OP.OPDEF:
case Macro10.PSEUDO_OP.REPEAT:
this.defMacro(sOperator, sRemainder);
break;
case Macro10.PSEUDO_OP.PURGE:
this.delSymbols(sOperands);
break;
case Macro10.PSEUDO_OP.LALL: // TODO
case Macro10.PSEUDO_OP.LIST: // TODO
case Macro10.PSEUDO_OP.NOSYM: // TODO
case Macro10.PSEUDO_OP.PAGE: // TODO
case Macro10.PSEUDO_OP.SUBTTL: // TODO
case Macro10.PSEUDO_OP.TITLE: // TODO
case Macro10.PSEUDO_OP.XALL: // TODO
case Macro10.PSEUDO_OP.XLIST: // TODO
break;
default:
this.defWord(sOperator, sSeparator, sOperands);
break;
}
}
return true;
}
/**
* parseLiteral(name, sText)
*
* This is like parseText() except that we set up a new scope, including new aWords and aFixups arrays.
*
* @this {Macro10}
* @param {string} name
* @param {string} sText
*/
parseLiteral(name, sText)
{
this.pushScope(name);
this.parseText(sText);
this.popScope();
}
/**
* parseMacro(name, sOperands)
*
* For OPDEF macros, the operands are opcode values rather than conventional macro parameter values.
* As the MACRO-10 manual explains:
*
* Defines the symbol as an operator equivalent to expression, giving the symbol a fullword value.
* When the operator is later used with operands, the accumulator fields are added, the indirect bits
* are ORed, the memory addresses are added, and the index register addresses are added.
*
* EXAMPLE: OPDEF CAL [MOVE 1,@SYM(2)]
* CAL 1,BOL(2)
*
* RESULT: MOVE 2,@SYM+BOL(4)
*
* The easiest thing to do is parse the OPDEF text, allowing it to generate its default "fullword value",
* then parse the operands in their own scope, extract the bits generated from the operands, and merge them
* into the generated OPDEF value.
*
* @this {Macro10}
* @param {string} name
* @param {string} [sOperands]
* @return {boolean}
*/
parseMacro(name, sOperands)
{
var macro = this.tblMacros[name];
if (!macro) return false;
if (sOperands != null) {
/*
* If this is an OPDEF, then a two-step process is required: generate the OPDEF's value, then parse
* the operands and merge their bits with the OPDEF's bits.
*/
if (macro.nOperand == Macro10.MACRO_OP.OPDEF) {
var nLocation = this.nLocation;
this.parseText(macro.sText);
if (nLocation < this.nLocation) {
/*
* An OPDEF invocation *may* have operands, but it's not required to.
*/
if (!sOperands) return true;
this.pushScope();
this.parseText(sOperands);
var w = this.aWords[0];
var sFixup = this.aFixups[0];
this.popScope();
if (w !== undefined) {
this.aWords[nLocation] += (w & (PDP10.OPCODE.A_FIELD | PDP10.OPCODE.X_FIELD | PDP10.OPCODE.Y_FIELD));
/*
* We can't "OR" (|=) the I_FIELD bit into the target word, because it's a 36-bit value and bitwise
* operators truncate to 32 bits, so we'll use addition, trusting that the field was initially zero.
*/
this.aWords[nLocation] += (w & PDP10.OPCODE.I_FIELD);
if (sFixup) {
if (!this.aFixups[nLocation]) {
this.aFixups[nLocation] = sFixup;
} else {
this.aFixups[nLocation] += '+' + sFixup;
}
}
return true;
}
}
/*
* Either the OPDEF didn't generate any data OR the OPDEF's operands failed to evaluate.
*/
this.error("OPDEF '" + name + "' (" + sOperands + ") failed");
return false;
}
var macroPrev = this.macroCall;
this.macroCall = macro;
macro.aValues = this.getValues(sOperands, true);
this.parseText(macro.sText, macro.aParms, macro.aValues, macro.aDefaults);
/*
* WARNING: Our simplistic approach to macro expansion and processing means that recursive macros
* (such as the SHIFT macro in /apps/pdp10/tests/macro10/TEXT.MAC) could blow the stack. Nothing bad
* should happen (other than a JavaScript stack limit exception aborting the assembly), but it begs
* the question: did MACRO-10 perform any tail recursion optimizations or other tricks to prevent macros
* from gobbling stack, or could they blow MACRO-10's stack just as easily?
*/
this.macroCall = macroPrev;
return true;
}
if (name[0] != '?') return false;
var sOperator = name.substr(1);
switch(sOperator) {
case Macro10.PSEUDO_OP.IFE:
case Macro10.PSEUDO_OP.IFDIF:
case Macro10.PSEUDO_OP.IFNDEF:
if (!macro.nOperand) {
this.parseText(macro.sText);
}
break;
case Macro10.PSEUDO_OP.IFG:
if (macro.nOperand > 0) {
this.parseText(macro.sText);
}
break;
case Macro10.PSEUDO_OP.IFGE:
if (macro.nOperand >= 0) {
this.parseText(macro.sText);
}
break;
case Macro10.PSEUDO_OP.IFL:
if (macro.nOperand < 0) {
this.parseText(macro.sText);
}
break;
case Macro10.PSEUDO_OP.IFLE:
if (macro.nOperand <= 0) {
this.parseText(macro.sText);
}
break;
case Macro10.PSEUDO_OP.IF1:
case Macro10.PSEUDO_OP.IFN:
case Macro10.PSEUDO_OP.IFDEF:
case Macro10.PSEUDO_OP.IFIDN:
if (macro.nOperand) {
this.parseText(macro.sText);
}
break;
case Macro10.PSEUDO_OP.IRP:
case Macro10.PSEUDO_OP.IRPC:
for (let i = 0; i < macro.aValues.length; i++) {
this.parseText(macro.sText, macro.aParms, [macro.aValues[i]], []);
}
break;
case Macro10.PSEUDO_OP.REPEAT:
while (macro.nOperand-- > 0) {
this.parseText(macro.sText);
}
break;
default:
this.parseLiteral(name, macro.sText);
break;
}
return true;
}
/**
* parseText(sText, aParms, aValues, aDefaults)
*
* @this {Macro10}
* @param {string} sText
* @param {Array.<string>} [aParms]
* @param {Array.<string>} [aValues]
* @param {Array.<string>} [aDefaults]
*/
parseText(sText, aParms, aValues, aDefaults)
{
/*
* Unlike the caller of parseResources(), we don't split the text using a capture group,
* so all line separators are tossed, and just like parseResources(), we always include a
* uniform CR/LF sequence at the end of each line.
*
* TODO: Consider whether callers should always store their text snippets as line arrays, too,
* to avoid this re-splitting.
*/
var asLines = sText.split(/\r?\n/);
for (var iLine = 0; iLine < asLines.length; iLine++) {
var sLine = asLines[iLine] + '\r\n';
if (!this.parseLine(sLine, aParms, aValues, aDefaults)) break;
}
}
/**
* pushScope(name)
*
* @this {Macro10}
* @param {string} [name] (must be defined for literals only)
*/
pushScope(name)
{
this.stackScopes.push({
name,
aWords: this.aWords,
aFixups: this.aFixups,
nLocation: this.nLocation,
nLocationScope: this.nLocationScope,
nLine: this.nLine
});
this.aWords = [];
this.aFixups = [];
if (this.nLocationScope < 0) this.nLocationScope = this.nLocation;
this.nLocation = 0;
}
/**
* popScope()
*
* @this {Macro10}
*/
popScope()
{
if (!this.stackScopes.length) {
this.error("scope nesting error");
return;
}
var name = this.stackScopes[this.stackScopes.length - 1].name;
if (name) this.aLiterals.push({name, aWords: this.aWords, aFixups: this.aFixups});
var scope = this.stackScopes.pop();
this.aWords = scope.aWords;
this.aFixups = scope.aFixups;
this.nLocation = scope.nLocation;
this.nLocationScope = scope.nLocationScope;
if (!this.stackScopes.length && this.nLocationScope != -1) {
this.error("scope restore error");
}
}
/**
* getExpression(sOperands, sDelim)
*
* TODO: Add support for IFIDN, IFDIF, IFB and IFNB: if the first non-blank, non-tab character is
* a character other than '<', then that becomes the delimiter, allowing angle brackets in the string.
*
* @this {Macro10}
* @param {string} sOperands
* @param {string} [sDelim] (eg, comma, closing parenthesis)
* @return {string|null} (if the operands begin with an expression, return it)
*/
getExpression(sOperands, sDelim = ",")
{
var i = 0;
var fQuotes = false;
var sOperand = null;
var cNesting = 0;
while (i < sOperands.length) {
var ch = sOperands[i++];
if (ch == '"') {
fQuotes = !fQuotes;
continue;
}
if (fQuotes) continue;
if (!cNesting && sDelim.indexOf(ch) >= 0) {
i--;
break;
}
if (ch == '<') {
cNesting++;
} else if (ch == '>') {
if (--cNesting < 0) {
this.error("missing bracket(s) in '" + sOperands + "'");
break;
}
}
}
if (!cNesting) {
sOperand = sOperands.substr(0, i);
}
else if (cNesting > 0) {
this.error("extra bracket(s) in '" + sOperands + "'");
}
return sOperand;
}
/**
* parseExpression(sExp, aUndefined, nLocation, nLine)
*
* This is a wrapper around the Debugger's parseExpression() function to take care of some
* additional requirements we have, such as interpreting a period as the current location and
* interpreting two expressions separated by two commas as the left and right 18-bit halves
* of a 36-bit value.
*
* @this {Macro10}
* @param {string} sExp
* @param {Array} [aUndefined]
* @param {number|undefined} [nLocation]
* @param {number|undefined} [nLine]
* @return {number|undefined}
*/
parseExpression(sExp, aUndefined, nLocation, nLine)
{
var result = -1;
if (nLocation === undefined) {
nLocation = (this.nLocationScope >= 0? this.nLocationScope : this.nLocation);
}
/*
* The SIXBIT (and presumably ASCII; not sure about ASCIZ) pseudo-ops can also be used in expressions
* (or at least assignments), so we check for those in the given expression and convert them to quoted
* sequences that the Debugger's parseExpression() understands.
*/
var sEval = sExp.replace(/SIXBIT\s*(\S)(.*?)\1/g, "'$2'").replace(/ASCII\s*(\S)(.*?)\1/g, '"$2"');
/*
* If this is NOT a first-pass call, then let's not waste time calling parseInstruction(); not only
* may it generate redundant error messages, but it shouldn't be necessary, because we should be down
* to fixup expressions.
*/
if (aUndefined) {
var match;
var sOperator = "";
var sOperands = sEval;
if (match = sEval.match(/^([^\s]+)\s*(.*?)\s*$/)) {
sOperator = match[1];
sOperands = match[2];
}
result = this.dbg.parseInstruction(sOperator, sOperands, nLocation, aUndefined);
}
if (result < 0) {
/*
* Check for the "period" syntax that MACRO-10 uses to represent the value of the current location.
* The Debugger's parseInstruction() method understands that syntax, but its parseExpression() method
* does not.
*
* Note that the Debugger's parseInstruction() replaces any period not PRECEDED by a decimal
* digit with the current address, because our Debuggers' only other interpretation of a period
* is as the suffix of a decimal integer, whereas MACRO-10's only other interpretation of a period
* is (I think) as the decimal point within a floating-point number, so here we only replace periods
* that are not FOLLOWED by a decimal digit.
*/
sEval = sEval.replace(/\.([^0-9]|$)/g, this.dbg.toStrBase(nLocation, -1) + "$1");
result = this.dbg.parseExpression(sEval, aUndefined);
if (result === undefined) {
this.error("unable to parse expression '" + sExp + "'", nLine);
}
}
return result;
}
/**
* getLiteral(sOperands)
*
* Check the operands for a literal (ie, an expression starting with a square bracket).
*
* @this {Macro10}
* @param {string} sOperands
* @return {string} (if the operands contain a literal, return it)
*/
getLiteral(sOperands)
{
var cNesting = 0;
var sLiteral = "";
var i = 0, iBegin = -1, iEnd = sOperands.length;
while (i < sOperands.length) {
var ch = sOperands[i];
if (ch == ';') break;
if (ch == '[') {
if (!cNesting++) iBegin = i;
}
i++;
if (ch == ']' && --cNesting <= 0) {
iEnd = i;
break;
}
}
if (cNesting < 0) {
this.error("missing bracket(s) in '" + sOperands + "'");
}
if (iBegin >= 0) {
sLiteral = sOperands.substr(iBegin, iEnd - iBegin);
}
return sLiteral;
}
/**
* getDefaults(aParms)
*
* Check the given array of macro parameters for default values, remove them, and return them in a parallel array.
*
* @this {Macro10}
* @param {Array.<string>} aParms
* @return {Array.<string>}
*/
getDefaults(aParms)
{
var aDefaults = [];
for (var i = 0; i < aParms.length; i++) {
var j = aParms[i].indexOf('<');
if (j >= 0) {
var k = aParms[i].lastIndexOf('>');
if (k < 0) k = aParms[i].length;
aDefaults[i] = aParms[i].substr(j, k - j);
aParms[i] = aParms[i].substr(0, j);
}
}
return aDefaults;
}
/**
* getLineRef(nLine)
*
* @this {Macro10}
* @param {number|undefined} [nLine]
* @return {string}
*/
getLineRef(nLine = this.nLine)
{
var iURL = 0;
while (nLine > this.anLines[iURL]) {
nLine -= this.anLines[iURL];
if (iURL < this.aURLs.length - 1) {
iURL++;
} else {
break;
}
}
return Str.getBaseName(this.aURLs[iURL] + " line " + nLine);
}
/**
* getReserved(sOperands)
*
* Check the operands for any reserved symbols (ie, symbols with a trailing '#', such as "USER#").
*
* @this {Macro10}
* @param {string} sOperands
* @return {string|null} (if the operands contain a reserved symbol, return it)
*/
getReserved(sOperands)
{
var match, sReserved = null;
if (match = sOperands.match(/([A-Z$%.][0-9A-Z$%.]*)#/i)) {
sReserved = match[0];
var sLabel = match[1];
var name = '?' + sLabel;
/*
* We now allow reserved symbols to reused (which would make sense if they appeared in a macro).
*/
if (this.tblMacros[name] === undefined) {
var aParms, aDefaults, aValues;
aParms = aDefaults = aValues = [];
this.tblMacros[name] = {
name: name,
nOperand: Macro10.MACRO_OP.RESERVED,
aParms,
aDefaults,
aValues,
sText: sLabel + ": 0",
nLine: this.nLine
};
this.aVariables.push(name);
}
}
return sReserved;
}
/**
* getString(sValue, nConversion)
*
* Converts the given expression string (sValue) to one of the following, based on the conversion code (nConversion):
*
* 0: numeric string (default)
* 1: SIXBIT string
* 2: ASCII string
*
* If the expression cannot be evaluated, or if the requested conversion isn't recognized, the original value is returned.
*
* @this {Macro10}
* @param {string} sValue
* @param {number} [nConversion]
* @return {string}
*/
getString(sValue, nConversion = 0)
{
var c, s;
var value = this.parseExpression(sValue);
if (value !== undefined) {
var cchMax = 5;
switch(nConversion) {
case 0:
sValue = this.dbg.toStrBase(value, -1);
break;
case 6:
cchMax++;
/* falls through */
case 7:
s = "";
while (value && cchMax--) {
c = value & (Math.pow(2, nConversion) - 1);
s = String.fromCharCode(c + (nConversion == 6? 0x20 : 0)) + s;
value = Math.trunc(value / Math.pow(2, nConversion));
}
break;
}
}
return sValue;
}
/**
* getSymbol(sOperands)
*
* Check the operands for a symbol. TODO: Use this method?
*
* @this {Macro10}
* @param {string} sOperands
* @return {string|null} (if the operands contain a symbol, return it)
*/
getSymbol(sOperands)
{
var match = sOperands.match(/([A-Z$%.][0-9A-Z$%.]*)/i);
return match && match[1] || null;
}
/**
* getValues(sOperands, fParens)
*
* @this {Macro10}
* @param {string} sOperands
* @param {boolean} [fParens] (true to strip any parens from around the entire operands)
* @return {Array.<string>}
*/
getValues(sOperands, fParens)
{
var aValues = [];
if (fParens) sOperands = sOperands.replace(/^\(?(.*?)\)?$/, "$1");
while (sOperands) {
sOperands = sOperands.trim();
var sOperand = this.getExpression(sOperands);
if (!sOperand) break;
var sValue = sOperand;
if (sOperand[0] == '\\') {
var cchPrefix = 1;
var nConversion = 0;
if (sOperand[1] == "'") {
cchPrefix++;
nConversion = 6;
} else if (sOperand[1] == '"') {
cchPrefix++;
nConversion = 7;
}
sValue = this.getString(sOperand.substr(cchPrefix), nConversion);
}
aValues.push(sValue);
sOperands = sOperands.substr(sOperand.length + 1);
}
return aValues;
}
/**
* isDefined(sName)
*
* @this {Macro10}
* @param {string} sName
* @return {boolean}
*/
isDefined(sName)
{
return this.isMacro(sName) || this.isSymbol(sName) || this.dbg.isVariable(sName);
}
/**
* isMacro(sName)
*
* @this {Macro10}
* @param {string} sName
* @return {boolean}
*/
isMacro(sName)
{
return this.tblMacros[sName] !== undefined;
}
/**
* isSymbol(sName)
*
* @this {Macro10}
* @param {string} sName
* @return {boolean}
*/
isSymbol(sName)
{
return this.tblSymbols[sName] !== undefined;
}
/**
* isSymbolChar(ch)
*
* @this {Macro10}
* @param {string} ch
* @return {boolean}
*/
isSymbolChar(ch)
{
return !!ch.match(/[0-9A-Z$%.]/i);
}
/**
* defASCII(sOperands)
*
* @this {Macro10}
* @param {string} sOperands
* @return {string} (returns whatever portion of the string was not part of an ASCII pseudo-op)
*/
defASCII(sOperands)
{
var sRemain = sOperands;
if (this.chASCII == null) {
this.chASCII = this.sASCII = "";
if (sOperands) {
this.chASCII = sOperands[0];
sRemain = sOperands = sOperands.substr(1);
}
}
if (this.chASCII) {
var i = sOperands.indexOf(this.chASCII);
if (i < 0) {
sRemain = "";
} else {
sRemain = sOperands.substr(i + 1);
sOperands = sOperands.substr(0, i);
this.chASCII = null;
}
this.sASCII += sOperands;
}
if (this.chASCII == null) {
this.genASCII();
}
return sRemain;
}
/**
* defMacro(sOperator, sOperands)
*
* If sOperator is DEFINE (or OPDEF), then a macro definition is expected. If it's REPEAT, then we're
* starting a REPEAT block instead.
*
* REPEAT blocks piggy-back on this code because they're essentially anonymous immediately-invoked macros;
* we use an illegal MACRO-10 symbol ('?REPEAT') to name the anonymous macro while it's being defined, and the
* macro's nOperand field will contain the repeat count.
*
* The piggy-backing continues with other pseudo-ops like IFE, which again contain an anonymous block of text
* that is immediately invoked if the criteria associated with the expression stored in the nOperand field is
* satisfied. That satisfaction occurs (or doesn't occur) when parseMacro() is called, once the macro has
* been fully defined.
*
* @this {Macro10}
* @param {string} sOperator
* @param {string} sOperands
* @return {string}
*/
defMacro(sOperator, sOperands)
{
var i, match, name, nOperand, aParms, aDefaults, aValues, iMatch;
this.chMacroOpen = '<';
this.chMacroClose = '>';
aParms = aDefaults = aValues = [];
if (sOperator == Macro10.PSEUDO_OP.DEFINE) {
/*
* This is a DEFINE (macro) block. At present, this requires that all
* (parenthesized) parameters exist on the same line, but the (angle-bracketed)
* definition can continue on for multiple lines.
*/
match = sOperands.match(/([A-Z$%.][0-9A-Z$%.]*)\s*(\([^)]*\)|)\s*,?\s*(<|)([\s\S]*)/i);
if (!match) {
this.error("unrecognized " + sOperator + " in '" + sOperands + "'");
return sOperands;
}
name = match[1];
/*
* If this macro has defined parameters, parse them (and any defaults) now.
*/
if (match[2] && match[2] != ',') {
aParms = this.getValues(match[2], true);
aDefaults = this.getDefaults(aParms);
}
nOperand = Macro10.MACRO_OP.DEFINE;
iMatch = 3;
}
else if (sOperator == Macro10.PSEUDO_OP.OPDEF) {
/*
* This is a OPDEF block. Unlike DEFINE blocks, I'm assuming that the
* (square-bracketed) definition begins on the same line, but I'm not requiring
* it to end on the same line (I'm not sure that MACRO-10 allowed multi-line
* OPDEFs, but it doesn't matter to us).
*/
this.chMacroOpen = '[';
this.chMacroClose = ']';
match = sOperands.match(/([A-Z$%.][0-9A-Z$%.]*)\s*(\[)([\s\S]*)/i);
if (!match) {
this.error("unrecognized " + sOperator + " in '" + sOperands + "'");
return sOperands;
}
name = match[1];
nOperand = Macro10.MACRO_OP.OPDEF;
iMatch = 2;
}
else if (sOperator == Macro10.PSEUDO_OP.LITERAL) {
/*
* This is a LITERAL block.
*/
this.chMacroOpen = '[';
this.chMacroClose = ']';
name = '?' + Str.toDec(++this.nLiteral, 5);
if (this.tblMacros[name] !== undefined) {
this.error("literal symbol '" + name + "' redefined");
}
match = [sOperands[0], sOperands.substr(1)];
nOperand = Macro10.MACRO_OP.LITERAL;
iMatch = 0;
}
else if (sOperator == Macro10.PSEUDO_OP.IRP || sOperator == Macro10.PSEUDO_OP.IRPC) {
/*
* IRP (and IRPC) blocks are very similar to DEFINE blocks, but they define exactly ONE macro parameter
* with NO parentheses (whereas regular macros always define their parameters, if any, WITH parentheses),
* and then the IRP (or IRPC) block is immediately invoked with the corresponding value from the
* enclosing macro.
*/
if (!this.macroCall) {
this.error(sOperator + " outside of macro");
}
match = sOperands.match(/([A-Z$%.][0-9A-Z$%.]*)\s*,\s*(<|)([\s\S]*)/i);
if (!match) {
this.error("unrecognized " + sOperator + " operands '" + sOperands + "'");
return sOperands;
}
for (i = 0; i < this.macroCall.aParms.length; i++) {
if (match[1] == this.macroCall.aParms[i]) break;
}
if (i == this.macroCall.aParms.length) {
this.error("invalid " + sOperator + " parameter '" + match[1] + "'");
return sOperands;
}
name = '?' + sOperator;
aParms = [match[1]];
if (sOperator == Macro10.PSEUDO_OP.IRPC) {
aValues = this.macroCall.aValues[i].split("");
} else {
aValues = this.getValues(this.macroCall.aValues[i]);
}
nOperand = aValues.length;
iMatch = 2;
}
else {
/*
* This must be a REPEAT or CONDITIONAL block.
*/
if (sOperator == Macro10.PSEUDO_OP.IF1) {
nOperand = 1;
if (sOperands[0] == ',') sOperands = sOperands.substr(1).trim();
}
else {
var sOperand = this.getExpression(sOperands);
if (!sOperand) {
this.error("missing " + sOperator + " expression '" + sOperands + "'");
return sOperands;
}
sOperands = sOperands.substr(sOperand.length + 1);
sOperand = sOperand.trim();
if (sOperator == Macro10.PSEUDO_OP.IFDIF || sOperator == Macro10.PSEUDO_OP.IFIDN) {
var sOperand2 = this.getExpression(sOperands);
if (!sOperand2) {
this.error("missing second " + sOperator + " expression '" + sOperands + "'");
return sOperands;
}
sOperands = sOperands.substr(sOperand2.length + 1);
sOperand2 = sOperand2.trim();
nOperand = (sOperand == sOperand2)? 1 : 0;
}
if (sOperator == Macro10.PSEUDO_OP.IFDEF || sOperator == Macro10.PSEUDO_OP.IFNDEF) {
nOperand = this.isDefined(sOperand)? 1 : 0;
} else {
/*
* The expression is either a repeat count or a condition. Either way, we must be able to
* resolve it now, so we don't set fPass1 (but that doesn't mean it's the second pass, either).
*/
nOperand = this.parseExpression(sOperand) || 0;
}
}
match = sOperands.match(/\s*(<|)([\s\S]*)/i);
name = '?' + sOperator;
iMatch = 1;
}
/*
* Now we need to set a global parsing state: we are either about to receive a macro definition on
* subsequent lines (1), the definition has already started on the current line (2), or the definition
* started and ended on the current line (0).
*/
name = name.toUpperCase();
this.nMacroDef = 1;
this.sMacroDef = name;
this.tblMacros[name] = {name, nOperand, aParms, aDefaults, aValues, sText: "", nLine: this.nLine};
if (match[iMatch]) { // if there is an opening bracket
this.nMacroDef = 2; // then the macro definition has started
this.appendMacro(match[iMatch + 1]);
}
return name;
}
/**
* appendMacro(sLine)
*
* @this {Macro10}
* @param {string} sLine
* @return {string}
*/
appendMacro(sLine)
{
var sRemain = "";
for (var i = 0; i < sLine.length; i++) {
if (sLine[i] == this.chMacroOpen) {
this.nMacroDef++;
} else if (sLine[i] == this.chMacroClose) {
this.nMacroDef--;
if (this.nMacroDef == 1) {
this.nMacroDef = 0;
sRemain = sLine.substr(i + 1);
sLine = sLine.substr(0, i);
break;
}
}
}
var name = this.sMacroDef || "";
this.tblMacros[name].sText += sLine;
if (!this.nMacroDef) {
this.sMacroDef = null;
this.parseMacro(name);
}
return sRemain;
}
/**
* addSymbol(name, value, nType)
*
* @this {Macro10}
* @param {string} name
* @param {number|string} value
* @param {number} [nType]
*/
addSymbol(name, value, nType = 0)
{
name = name.toUpperCase().substr(0, 6);
if ((nType & Macro10.SYMTYPE.LABEL) && this.tblSymbols[name] !== undefined) {
this.error("redefined label '" + name + "'");
return;
}
var sUndefined = undefined;
if (typeof value == 'string') {
var aUndefined = [];
var v = this.parseExpression(value, aUndefined);
if (v === undefined) {
this.error("invalid symbol '" + name + "': " + value);
return;
}
if (aUndefined.length > 1) {
this.error("too many undefined symbols in '" + name + "': " + aUndefined.join());
}
value = v;
sUndefined = aUndefined[0];
}
var sym = this.tblSymbols[name];
if (sym) {
sym.value = value;
sym.nType = nType;
sym.nLine = this.nLine;
} else {
this.tblSymbols[name] = {name, value, nType, nLine: this.nLine};
}
this.dbg.setVariable(name, value, sUndefined);
}
/**
* defBLOCK()
*
* Processes the BLOCK pseudo-op.
*
* @this {Macro10}
* @param {string} sOperands
*/
defBLOCK(sOperands)
{
var w = this.parseExpression(sOperands);
if (w !== undefined && w >= 0 && w <= 0o777777) {
this.nLocation += w; // while (w--) this.genWord(0);
} else {
this.error("unrecognized BLOCK expression '" + sOperands + "'");
}
}
/**
* defBYTE()
*
* Processes the BYTE pseudo-op.
*
* @this {Macro10}
* @param {string} sOperands
*/
defBYTE(sOperands)
{
var sOperand;
var nBits = 0, nValue = 0, nBitsRemaining = 36;
while (sOperand = this.getExpression(sOperands)) {
sOperands = sOperands.substr(sOperand.length).trim();
var sValue = sOperand;
var match = sOperand.match(/^\((.*)\)\s*(.*)$/);
if (match) {
if (match[1]) nBits = this.parseExpression("^D" + match[1]);
sValue = match[2];
}
if (!nBits) {
/*
* According the the 1978 MACRO-10 spec, "If the byte size is 0 or is missing (empty parentheses), a zero word
* is generated." I'm not clear exactly on how to interpret this, so I'll make a simplistic assumption for now.
*/
this.genWord(0);
continue;
}
var v = sValue? this.parseExpression(sValue) : 0;
if (v === undefined || nBits < 0 || nBits > 36) {
this.error("unexpected BYTE operand: " + sOperand);
break;
}
v = this.dbg.truncate(v, nBits, true);
if (nBitsRemaining < nBits) {
this.genWord(nValue);
nValue = 0;
nBitsRemaining = 36;
}
nValue += v * Math.pow(2, nBitsRemaining - nBits);
nBitsRemaining -= nBits;
}
if (nBitsRemaining < 36) {
this.genWord(nValue);
}
}
/**
* defEND()
*
* Processes the END pseudo-op.
*
* @this {Macro10}
* @param {string} sOperands
*/
defEND(sOperands)
{
if (!sOperands) {
this.addrStart = this.addrLoad;
} else {
this.addrStart = this.parseExpression(sOperands);
if (this.addrStart === undefined) {
this.error("unrecognized END expression '" + sOperands + "'");
}
}
}
/**
* defLocation(sOperands)
*
* Processes the LOC pseudo-op.
*
* @this {Macro10}
* @param {string} sOperands
*/
defLocation(sOperands)
{
this.nLocation = this.parseExpression(sOperands) || 0;
}
/**
* defXWD()
*
* Processes the XWD pseudo-op.
*
* Since the XWD pseudo-op appears to be equivalent to two values separated by two commas, which defWord() must also
* support, we can piggy-back on defWord().
*
* @this {Macro10}
* @param {string} sOperands
*/
defXWD(sOperands)
{
this.defWord(sOperands.replace(",", ",,"));
}
/**
* defWord(sOperator, sSeparator, sOperands)
*
* @this {Macro10}
* @param {string} sOperator
* @param {string} [sSeparator]
* @param {string} [sOperands]
*/
defWord(sOperator, sSeparator = "", sOperands = "")
{
var aUndefined = [];
var sExp = (sOperator + sSeparator + sOperands).trim();
var w = this.parseExpression(sExp, aUndefined);
if (w !== undefined) {
if (!aUndefined.length) {
this.genWord(w);
} else if (aUndefined.length == 1) {
this.genWord(w, aUndefined[0]);
}
else {
this.genWord(0, sExp);
}
} else {
this.error("unrecognized word expression '" + sExp + "'");
}
}
/**
* delSymbols(sOperands)
*
* @this {Macro10}
* @param {string} sOperands
*/
delSymbols(sOperands)
{
var aValues = this.getValues(sOperands);
for (var i = 0; i < aValues.length; i++) {
this.dbg.delVariable(aValues[i]);
delete this.tblSymbols[aValues[i]];
}
}
/**
* doLiterals()
*
* @this {Macro10}
*/
doLiterals()
{
let nLocationLiterals = this.nLocation;
for (let i = 0; i < this.aLiterals.length; i++) {
/*
* Apparently, the time has come to implement "literal collapsing"; I was treating it as just
* a nice optimization, but it turns out that DEC has written tests that actually DEPEND on it:
*
* C26300: HRRZI [135531,,246642] ;PRELOAD AC0 WITH 0,, LITERAL ADDRESS
* JRA .+1 ;*JRA SHOULD PLACE C(AC0) INTO AC0
* CAIE [135531,,246642] ;PASS IF JRA PLACED C(AC0) INTO AC0
* STOP
*
* If the HRRZI and CAIE instructions don't refer to the same exact literal, the test will fail.
* For purposes of this particular test, the values they stuffed into the literals are essentially
* gibberish, but the same literal may be used in another test where the values are significant.
*
* However, I'm still going to keep it simple. In this example from p. 2-8 of the April 1978
* MACRO-10 manual, I will NOT be attempting to collapse null words at the end of ASCIZ sequences
* with other null words, especially if they were defined before the ASCIZ:
*
* Literals having the same value are collapsed in MACRO's literal pool.
* Thus for the statements:
*
* PUSH P,[0]
* PUSH P,[0]
* MOVEI 1,[ASCIZ /TEST1/]
*
* the same address is shared by the two literals [0], and by the null word
* generated at the end of [ASCIZ /TEST1/].
*/
let lit = this.aLiterals[i];
/*
* First things first: verify that the literal is one contiguous zero-based set of words (I'm not sure
* how it couldn't be, but better safe than sorry).
*/
let aWords = [];
let nWords = 0;
lit.aWords.forEach(function(w, nLocation) {
if (nLocation === aWords.length) aWords.push(w);
nWords++;
});
if (nWords == aWords.length) {
/*
* So far, so good. Now we'll simply brute-force-search our way through the existing set of
* literals, looking for a complete match.
*/
for (let nLocation = nLocationLiterals; nLocation + nWords <= this.nLocation; nLocation++) {
let n;
for (n = 0; n < nWords; n++) {
/*
* This check requires that the initial values of the words in the literals match AND that
* their fixup expressions, if any, match as well. Here again, MACRO-10 may be more aggressive
* in either verifying fixup equality or evaluating any fixups that it can immediately, but
* but I prefer to leave all fixup evaluation where it is (below), after all literals and then
* all variables have been added to the output.
*/
if (aWords[n] !== this.aWords[nLocation + n] || lit.aFixups[n] != this.aFixups[nLocation]) break;
}
if (n == nWords) {
this.addSymbol(lit.name, nLocation, Macro10.SYMTYPE.LABEL);
lit = null;
break;
}
}
}
if (lit) {
var macro10 = this;
this.addSymbol(lit.name, this.nLocation, Macro10.SYMTYPE.LABEL);
lit.aWords.forEach(function(w, nLocation) {
macro10.genWord(w, lit.aFixups[nLocation]);
});
}
}
this.aLiterals = [];
}
/**
* doVariables()
*
* @this {Macro10}
*/
doVariables()
{
for (let i = 0; i < this.aVariables.length; i++) {
let name = this.aVariables[i];
let macro = this.tblMacros[name];
if (!macro) {
/*
* This is more of an assert(), because it should never happen, regardless of input.
*/
this.error("missing definition for variable '" + name + "'");
continue;
}
this.parseText(macro.sText);
}
this.aVariables = [];
}
/**
* genASCII()
*
* Based on the last operator, generate the appropriate ASCII/ASCIZ/SIXBIT data.
*
* @this {Macro10}
*/
genASCII()
{
var n = 0, w = 0; // number of characters in current word, and current word
var bits, shift; // bits per character, and bits to left-shift next character
var cch = this.sASCII.length;
if (this.sOperator == Macro10.PSEUDO_OP.ASCIZ) cch++;
for (var i = 0; i < cch; i++) {
if (!n) {
w = 0; shift = 29; bits = 7;
if (this.sOperator == Macro10.PSEUDO_OP.SIXBIT) {
bits--; shift++;
}
}
/*
* If we're processing an ASCIZ pseudo-op, then yes, we will fetch one character beyond
* the end of sASCII, which will return NaN, but when we mask a falsey value like NaN, we
* get zero, so it's all good.
*/
var c = this.sASCII.charCodeAt(i) & 0o177;
/*
* If we're doing 6-bit encoding, then perform the conversion of lower-case to upper-case,
* and then adjust/mask.
*/
if (bits == 6) {
if (c >= 0x61 && c <= 0x7A) c -= 0x20;
c = (c + 0o40) & 0o77;
}
w += c * Math.pow(2, shift);
shift -= bits;
n++;
if (shift < 0) {
this.genWord(w);
n = 0;
}
}
if (n) this.genWord(w);
}
/**
* genWord(value, sUndefined)
*
* @this {Macro10}
* @param {number} value (default value for the current location)
* @param {string} [sUndefined] (optional fixup to evaluate later)
*/
genWord(value, sUndefined)
{
this.aWords[this.nLocation] = this.truncate(value);
if (sUndefined !== undefined) this.aFixups[this.nLocation] = sUndefined;
this.aLineRefs[this.nLocation] = this.nLine;
this.nLocation++;
}
/**
* truncate(value, nLocation)
*
* @this {Macro10}
* @param {number} value
* @param {number} [nLocation]
* @return {number}
*/
truncate(value, nLocation = this.nLocation)
{
var w = this.dbg.truncate(value || 0, 36, true);
if (value < -PDP10.INT_LIMIT || value >= PDP10.WORD_LIMIT) {
this.warning("truncated value " + Str.toOct(value) + " at location " + Str.toOct(nLocation) + " to " + Str.toOct(w));
}
return w;
}
/**
* error(sError, nLine)
*
* @this {Macro10}
* @param {string} sError
* @param {number} [nLine]
* @throws {Error}
*/
error(sError, nLine)
{
throw new Error("error in " + this.getLineRef(nLine) + ": " + sError);
}
/**
* warning(sWarning, nLine)
*
* @this {Macro10}
* @param {string} sWarning
* @param {number} [nLine]
*/
warning(sWarning, nLine)
{
this.println("warning in " + this.getLineRef(nLine) + ": " + sWarning);
}
/**
* println(s)
*
* @this {Macro10}
* @param {string} s
*/
println(s)
{
if (!this.dbg) {
console.log(s);
} else {
this.dbg.println(s);
}
}
}
Macro10.SYMTYPE = {
LABEL: 0x01,
PRIVATE: 0x02,
INTERNAL: 0x04
};
Macro10.PSEUDO_OP = {
ASCII: "ASCII",
ASCIZ: "ASCIZ",
BLOCK: "BLOCK",
BYTE: "BYTE",
DEFINE: "DEFINE",
END: "END",
EXP: "EXP",
IF1: "IF1",
IFDEF: "IFDEF",
IFDIF: "IFDIF",
IFE: "IFE",
IFG: "IFG",
IFGE: "IFGE",
IFIDN: "IFIDN",
IFL: "IFL",
IFLE: "IFLE",
IFN: "IFN",
IFNDEF: "IFNDEF",
IRP: "IRP",
IRPC: "IRPC",
LALL: "LALL",
LIT: "LIT",
LITERAL: "LITERAL", // this is a pseudo-pseudo-op, for internal use only
LIST: "LIST",
LOC: "LOC",
NOSYM: "NOSYM",
OPDEF: "OPDEF",
PAGE: "PAGE",
PURGE: "PURGE",
REPEAT: "REPEAT",
SIXBIT: "SIXBIT",
SUBTTL: "SUBTTL",
TITLE: "TITLE",
VAR: "VAR",
XALL: "XALL",
XWD: "XWD",
XLIST: "XLIST"
};
/*
* This enumerates the kinds of macros stored in tblMacros. The nOperand field should contain
* one of these values, unless it's a REPEAT or CONDITIONAL block, in which case it will contain
* either a repeat count or conditional value.
*/
Macro10.MACRO_OP = {
DEFINE: -1,
OPDEF: -2,
LITERAL: -3,
RESERVED: -4,
};
/**
* @copyright http://pcjs.org/modules/pdp10/lib/computer.js (C) Jeff Parsons 2012-2017
*/
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".
*
* @this {ComputerPDP10}
* @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));
this.controlPrint = this.panel && this.panel.bindings['print'];
if (this.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.print = this.panel.print;
component.println = this.panel.println;
}
}
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);
}
/**
* clearPanel()
*
* @this {ComputerPDP10}
*/
clearPanel()
{
if (this.controlPrint) {
this.controlPrint.value = "";
}
}
/**
* getMachineID()
*
* @this {ComputerPDP10}
* @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).
*
* @this {ComputerPDP10}
* @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.
*
* @this {ComputerPDP10}
* @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()
*
* @this {ComputerPDP10}
* @return {string|null}
*/
saveMachineParms()
{
return this.parmsMachine? JSON.stringify(this.parmsMachine) : null;
}
/**
* getUserID()
*
* @this {ComputerPDP10}
* @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)");
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.
*/
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:
*
* $('<a href="' + sState + '" download="state.json">Download</a>').appendTo('#container');
*/
};
return true;
default:
break;
}
return false;
}
/**
* resetUserID()
*
* @this {ComputerPDP10}
*/
resetUserID()
{
Web.setLocalStorageItem(ComputerPDP10.STATE_USERID, "");
this.sUserID = null;
}
/**
* queryUserID(fPrompt)
*
* @this {ComputerPDP10}
* @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)
*
* @this {ComputerPDP10}
* @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) {
computer.flags.unloading = false;
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) {
/*
* Added a new flag that Component functions (eg, notice()) should check before alerting the user.
*/
computer.flags.unloading = true;
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);
/**
* @copyright http://pcjs.org/modules/shared/lib/state.js (C) Jeff Parsons 2012-2017
*/
class State {
/**
* State(component, sVersion, sSuffix)
*
* State objects are used by components to save/restore their state.
*
* During a save operation, components add data to a State object via set(), and then return
* the resulting data using data().
*
* During a restore operation, the Computer component passes the results of each data() call
* back to the originating component.
*
* WARNING: Since State objects are low-level objects that have no UI requirements, they do not
* inherit from the Component class, so you should only use class methods of Component, such as
* Component.assert() (or Debugger methods if the Debugger is available).
*
* NOTE: 1.01 is the first version to provide limited save/restore support using localStorage.
* From that point on, care must be taken to insure that any new version that's incompatible with
* previous localStorage data be released with a version number that is at least 1 greater,
* since we're tagging the localStorage data with the integer portion of the version string.
*
* @param {Component} component
* @param {string} [sVersion] is used to append a major version number to the key
* @param {string} [sSuffix] is used to append any additional suffixes to the key
*/
constructor(component, sVersion, sSuffix)
{
this.id = component.id;
this.dbg = component.dbg;
this.json = "";
this.state = {};
this.fLoaded = this.fParsed = false;
this.key = State.key(component, sVersion, sSuffix);
this.unload(component.parms);
}
/**
* set(id, data)
*
* @this {State}
* @param {number|string} id
* @param {Object|string} data
*/
set(id, data)
{
try {
this.state[id] = data;
} catch(e) {
Component.log(e.message);
}
}
/**
* get(id)
*
* @this {State}
* @param {number|string} id
* @return {Object|string|null}
*/
get(id)
{
return this.state[id] || null;
}
/**
* data()
*
* @this {State}
* @return {Object}
*/
data()
{
return this.state;
}
/**
* load(json)
*
* WARNING: Make sure you follow this call with either a call to parse() or unload(),
* because any stringified data that we've loaded isn't usable until it's been parsed.
*
* @this {State}
* @param {string|null} [json]
* @return {boolean} true if state exists in localStorage, false if not
*/
load(json)
{
if (json) {
this.json = json;
this.fLoaded = true;
this.fParsed = false;
return true;
}
if (this.fLoaded) {
/*
* This is assumed to be a redundant load().
*/
return true;
}
if (Web.hasLocalStorage()) {
var s = Web.getLocalStorageItem(this.key);
if (s) {
this.json = s;
this.fLoaded = true;
if (DEBUG) Component.log("localStorage(" + this.key + "): " + s.length + " bytes loaded");
return true;
}
}
return false;
}
/**
* parse()
*
* This completes the load() operation, by parsing what was loaded, on the assumption there
* might be some benefit to deferring parsing until we've given the user a chance to confirm.
* Otherwise, load() could have just as easily done this, too.
*
* @this {State}
* @return {boolean} true if successful, false if error
*/
parse()
{
var fSuccess = true;
if (!this.fParsed) {
try {
this.state = JSON.parse(this.json);
this.fParsed = true;
} catch (e) {
Component.error(e.message || e);
fSuccess = false;
}
}
return fSuccess;
}
/**
* store()
*
* @this {State}
* @return {boolean} true if successful, false if error
*/
store()
{
var fSuccess = true;
if (Web.hasLocalStorage()) {
var s = JSON.stringify(this.state);
if (Web.setLocalStorageItem(this.key, s)) {
if (DEBUG) Component.log("localStorage(" + this.key + "): " + s.length + " bytes stored");
} else {
/*
* WARNING: Because browsers tend to disable all alerts() during an "unload" operation,
* it's unlikely anyone will ever see the "quota" errors that occur at this point. Need to
* think of some way to notify the user that there's a problem, and offer a way of cleaning
* up old states.
*/
Component.error("Unable to store " + s.length + " bytes in browser local storage");
fSuccess = false;
}
}
return fSuccess;
}
/**
* toString()
*
* @this {State}
* @return {string} JSON-encoded state
*/
toString()
{
return this.state? JSON.stringify(this.state) : this.json;
}
/**
* unload(parms)
*
* This discards any data saved via set() or loaded via load(), creating an empty State object.
* Note that you have to follow this call with an explicit call to store() if you want to remove
* the state from localStorage as well.
*
* @this {State}
* @param {Object} [parms]
*/
unload(parms)
{
this.json = "";
this.state = {};
this.fLoaded = this.fParsed = false;
if (parms) this.set("parms", parms);
}
/**
* clear(fAll)
*
* This unloads the current state, and then clears ALL localStorage for the current machine,
* independent of version, to reduce the chance of orphaned states wasting part of our limited allocation.
*
* @this {State}
* @param {boolean} [fAll] true to unconditionally clear ALL localStorage for the current domain
*/
clear(fAll)
{
this.unload();
var aKeys = Web.getLocalStorageKeys();
for (var i = 0; i < aKeys.length; i++) {
var sKey = aKeys[i];
if (sKey && (fAll || sKey.substr(0, this.key.length) == this.key)) {
Web.removeLocalStorageItem(sKey);
if (DEBUG) Component.log("localStorage(" + sKey + ") removed");
aKeys.splice(i, 1);
i = 0;
}
}
}
/**
* State.key(component, sVersion, sSuffix)
*
* This encapsulates the key generation code.
*
* @param {Component} component
* @param {string} [sVersion] is used to append a major version number to the key
* @param {string} [sSuffix] is used to append any additional suffixes to the key
* @return {string} key
*/
static key(component, sVersion, sSuffix)
{
var key = component.id;
if (sVersion) {
var i = sVersion.indexOf('.');
if (i > 0) key += ".v" + sVersion.substr(0, i);
}
if (sSuffix) {
key += "." + sSuffix;
}
return key;
}
/**
* State.compress(aSrc)
*
* @param {Array.<number>|null} aSrc
* @return {Array.<number>|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp)
*/
static compress(aSrc)
{
if (aSrc) {
var iSrc = 0;
var iComp = 0;
var aComp = [];
while (iSrc < aSrc.length) {
var n = aSrc[iSrc];
var iCompare = iSrc + 1;
while (iCompare < aSrc.length && aSrc[iCompare] === n) iCompare++;
aComp[iComp++] = iCompare - iSrc;
aComp[iComp++] = n;
iSrc = iCompare;
}
if (aComp.length < aSrc.length) return aComp;
}
return aSrc;
}
/**
* State.decompress(aComp)
*
* @param {Array.<number>} aComp
* @param {number} nLength is expected length of decompressed data
* @return {Array.<number>}
*/
static decompress(aComp, nLength)
{
var iDst = 0;
var aDst = new Array(nLength);
var iComp = 0;
while (iComp < aComp.length - 1) {
var c = aComp[iComp++];
var n = aComp[iComp++];
while (c--) {
aDst[iDst++] = n;
}
}
return aDst;
}
/**
* State.compressEvenOdd(aSrc)
*
* This is a very simple variation on compress() that compresses all the EVEN elements of aSrc first,
* followed by all the ODD elements. This tends to work better on EGA video memory, because when odd/even
* addressing is enabled (eg, for text modes), the DWORD values tend to alternate, which is the worst case
* for compress(), but the best case for compressEvenOdd().
*
* One wrinkle we support: if the first element is uninitialized, then we assume the entire array is undefined,
* and return an empty compressed array. Conversely, decompressEvenOdd() will take an empty compressed array
* and return an uninitialized array.
*
* @param {Array.<number>|null} aSrc
* @return {Array.<number>|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp)
*/
static compressEvenOdd(aSrc)
{
if (aSrc) {
var iComp = 0, aComp = [];
if (aSrc[0] !== undefined) {
for (var off = 0; off < 2; off++) {
var iSrc = off;
while (iSrc < aSrc.length) {
var n = aSrc[iSrc];
var iCompare = iSrc + 2;
while (iCompare < aSrc.length && aSrc[iCompare] === n) iCompare += 2;
aComp[iComp++] = (iCompare - iSrc) >> 1;
aComp[iComp++] = n;
iSrc = iCompare;
}
}
}
if (aComp.length < aSrc.length) return aComp;
}
return aSrc;
}
/**
* State.decompressEvenOdd(aComp, nLength)
*
* This is the counterpart to compressEvenOdd(). Note that because there's nothing in the compressed sequence
* that differentiates a compress() sequence from a compressEvenOdd() sequence, you simply have to be consistent:
* if you used even/odd compression, then you must use even/odd decompression.
*
* @param {Array.<number>} aComp
* @param {number} nLength is expected length of decompressed data
* @return {Array.<number>}
*/
static decompressEvenOdd(aComp, nLength)
{
var iDst = 0;
var aDst = new Array(nLength);
var iComp = 0;
while (iComp < aComp.length - 1) {
var c = aComp[iComp++];
var n = aComp[iComp++];
while (c--) {
aDst[iDst] = n;
iDst += 2;
}
/*
* The output of a "count,value" pair will never exceed the end of the output array, so as soon as we reach it
* the first time, we know it's time to switch to ODD elements, and as soon as we reach it again, we should be
* done.
*/
if (iDst == nLength) iDst = 1;
}
return aDst;
}
}
/**
* @copyright http://pcjs.org/modules/shared/lib/embed.js (C) Jeff Parsons 2012-2017
*/
/*
* We now support asynchronous XML and XSL file loads; simply set fAsync (below) to true.
*
* NOTE: For that support to work, we have to keep track of the number of machines on the page
* (ie, how many embedMachine() calls were issued), reduce the count as each machine XML file
* is fully transformed into HTML, and when the count finally returns to zero, notify all the
* machine component init() handlers.
*
* Also, to prevent those init() handlers from running prematurely, we must disable all page
* notification events at the start of the embedding process (Web.enablePageEvents(false)) and
* re-enable them at the end (Web.enablePageEvents(true)).
*/
var fAsync = true;
var cAsyncMachines = 0;
/**
* loadXML(sFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
*
* This is the preferred way to load all XML and XSL files. It uses getResource()
* to load them as strings, which parseXML() can massage before parsing/transforming them.
*
* For example, since I've been unable to get the XSLT document() function to work inside any
* XSL document loaded by JavaScript's XSLT processor, that has prevented me from dynamically
* loading any XML machine file that uses the "ref" attribute to refer to and incorporate
* another XML document.
*
* To solve that, I've added an fResolve parameter that tells parseXML() to fetch any
* referenced documents ITSELF and insert them into the XML string prior to parsing, instead
* of relying on the XSLT template to pull them in. That fetching is handled by resolveXML(),
* which iterates over the XML until all "refs" have been resolved (including any nested
* references).
*
* Also, XSL files with a <!DOCTYPE [...]> cause MSIE's Microsoft.XMLDOM.loadXML() function
* to choke, so I strip that out prior to parsing as well.
*
* TODO: Figure out why the XSLT document() function works great when the web browser loads an
* XML file (and the associated XSL file) itself, but does not work when loading documents via
* JavaScript XSLT support. Is it broken, is it a security issue, or am I just calling it wrong?
*
* @param {string} sXMLFile
* @param {string|null|undefined} idMachine
* @param {string|null|undefined} sAppName
* @param {string|null|undefined} sAppClass
* @param {string|null|undefined} sParms
* @param {boolean} fResolve is true to resolve any "ref" attributes
* @param {function(string)} display
* @param {function(string,Object)} done (string contains the unparsed XML string data, and Object contains a parsed XML object)
*/
function loadXML(sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
{
var doneLoadXML = function(sURLName, sXML, nErrorCode) {
if (nErrorCode) {
if (!sXML) sXML = "unable to load " + sXMLFile + " (" + nErrorCode + ")";
done(sXML, null);
return;
}
parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done);
};
display("Loading " + sXMLFile + "...");
Web.getResource(sXMLFile, null, fAsync, doneLoadXML);
}
/**
* parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
*
* Generates an XML document from an XML string. This function also provides a work-around for XSLT's
* lack of support for the document() function (at least on some browsers), by replacing every reference
* tag (ie, a tag with a "ref" attribute) with the contents of the referenced file.
*
* @param {string} sXML
* @param {string|null} sXMLFile
* @param {string|null|undefined} idMachine
* @param {string|null|undefined} sAppName
* @param {string|null|undefined} sAppClass
* @param {string|null|undefined} sParms
* @param {boolean} fResolve is true to resolve any "ref" attributes; default is false
* @param {function(string)} display
* @param {function(string,Object)} done (string contains the unparsed XML string data, and Object contains a parsed XML object)
*/
function parseXML(sXML, sXMLFile, idMachine, sAppName, sAppClass, sParms, fResolve, display, done)
{
var buildXML = function(sXML, sError) {
if (sError) {
done(sError, null);
return;
}
if (idMachine) {
/*
* A more sensible place to record the machine XML would be embedMachine(), like we do for the
* XSL file, but since we're about to modify the original machine XML, it's best to record it now.
*/
Component.addMachineResource(idMachine, sXMLFile, sXML);
var sURL = sXMLFile;
if (sURL && sURL.indexOf('/') < 0 && window.location.pathname.slice(-1) == '/') {
sURL = window.location.pathname + sURL;
}
/*
* We embed the URL of the XML file both as a separate "xml" attribute for easy access from the
* XSL file, and as part of the "parms" attribute for easy access from machines (see getMachineParm()).
*/
if (!sParms) {
sParms = '{';
} else if (sParms.slice(-1) == '}') {
sParms = sParms.slice(0, -1);
if (sParms.length > 1) sParms += ',';
} else { // sParms must just be a "state" file, so encode it as a "state" property
sParms = '{state:"' + sParms + '",';
}
sParms += 'url:"' + sURL + '"}';
/*
* Note that while we no longer generate a machine XML file with a "state" attribute (because it's
* encoded inside the "parms" attribute), the XSL file must still cope with "state" attributes inside
* other XML files; for example, manifest XML files like /apps/pc/1981/visicalc/manifest.xml contain
* machine elements with "state" attributes that must still be passed down to the computer element
* "the old fashioned way".
*
* Until/unless that changes, components.xsl cannot be simplified as much as I might have hoped.
*/
if (typeof resources == 'object') sURL = null; // turn off URL inclusion if we have embedded resources
sParms = sParms.replace(/\$/g, "$$$$");
sXML = sXML.replace(/(<machine[^>]*\sid=)(['"]).*?\2/, "$1$2" + idMachine + "$2" + (sParms? " parms='" + sParms + "'" : "") + (sURL? ' url="' + sURL + '"' : ''));
}
if (!fResolve) {
/*
* I'm trying to switch to a shared components.xsl (at least for all PC-class machines),
* but in the interim, that means hacking the XSL file on the fly to reflect the actual class.
*/
sXML = sXML.replace(/(<xsl:variable name="APPNAME">).*?(<\/xsl:variable>)/, "$1" + sAppName + "$2");
sXML = sXML.replace(/(<xsl:variable name="APPCLASS">).*?(<\/xsl:variable>)/, "$1" + sAppClass + "$2");
/*
* Non-COMPILED kludge to replace the version number template in the XSL file (which we assume we're reading,
* since fResolve is false) with whatever XMLVERSION we extracted from the XML file (see corresponding kludge below).
*
* ES6 ALERT: Template strings.
*/
if (!COMPILED && XMLVERSION) {
sXML = sXML.replace(/<xsl:variable name="APPVERSION">1.x.x<\/xsl:variable>/, `<xsl:variable name="APPVERSION">${XMLVERSION}</xsl:variable>`);
}
}
/*
* If the resource we requested is not really an XML file (or the file didn't exist and the server simply returned
* a message like "Cannot GET /devices/pc/machine/5150/cga/64kb/donkey/machine.xml"), we'd like to display a more
* meaningful message, because the XML DOM parsers will blithely return a document that contains nothing useful; eg:
*
* This page contains the following errors:error on line 1 at column 1:
* Document is empty Below is a rendering of the page up to the first error.
*
* Supposedly, the IE XML DOM parser will throw an exception, but I haven't tested that, and unless all other
* browsers do that, that's not helpful.
*
* The best I can do at this stage (assuming Web.getResource() didn't drop any error information on the floor)
* is verify that the requested resource "looks like" valid XML (in other words, it begins with a '<').
*/
var xmlDoc = null;
if (sXML.charAt(0) == '<') {
try {
/*
* Another hack for MSIE, which fails to load XSL documents containing a <!DOCTYPE [...]> tag.
*
* This is also why the XSLTProcessor 'transformToFragment' method in Microsoft Edge silently failed,
* so I had pull this hack out of the "ActiveXObject" code. And rather than add yet-another Microsoft
* browser check, I'm going to try doing this across the board, and hope that none of the other XSLT
* processors fail *without* the DOCTYPE tag.
*/
if (!fResolve) {
sXML = sXML.replace(/<!DOCTYPE(.|[\r\n])*]>\s*/g, "");
}
/*
* Beginning with Microsoft Edge and the corresponding release of Windows 10, all the
* 'ActiveXObject' crud has gone away; but of course, this code must remain in place if
* we want to continue supporting older Internet Explorer browsers (ie, back to IE9).
*/
/** @namespace window.ActiveXObject */
if (window.ActiveXObject || 'ActiveXObject' in window) { // second test is required for IE11 on Windows 8.1
xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async = false;
xmlDoc['loadXML'](sXML);
} else {
/** @namespace window.DOMParser */
xmlDoc = (new window.DOMParser()).parseFromString(sXML, "text/xml");
}
} catch(e) {
xmlDoc = null;
sXML = e.message;
}
} else {
sXML = "unrecognized XML: " + (sXML.length > 255? sXML.substr(0, 255) + "..." : sXML);
}
done(sXML, xmlDoc);
};
if (sXML) {
if (PRIVATE) sXML = sXML.replace(/\/library.xml/, "/private/library.xml");
if (fResolve) {
resolveXML(sXML, display, buildXML);
return;
}
buildXML(sXML, null);
return;
}
done("no data" + (sXMLFile? " for file: " + sXMLFile : ""), null);
}
/**
* resolveXML(sXML, display, done)
*
* Replaces every tag with a "ref" attribute with the contents of the corresponding file.
*
* TODO: Fix some of the limitations of this code, such as: 1) requiring the "ref" attribute
* to appear as the tag's first attribute, 2) requiring the "ref" attribute to be double-quoted,
* and 3) requiring the "ref" tag to be self-closing.
*
* @param {string} sXML
* @param {function(string)} display
* @param {function(string,(string|null))} done (the first string contains the resolved XML data, the second is for any error message)
*/
function resolveXML(sXML, display, done)
{
var matchRef;
var reRef = /<([a-z]+)\s+ref="(.*?)"(.*?)\/>/g;
if ((matchRef = reRef.exec(sXML))) {
var sRefFile = matchRef[2];
var doneReadXML = function(sURLName, sXMLRef, nErrorCode) {
if (nErrorCode || !sXMLRef) {
done(sXML, "unable to resolve XML reference: " + matchRef[0] + " (" + nErrorCode + ")");
return;
}
/*
* If there are additional attributes in the "referring" XML tag, we want to insert them
* into the "referred" XML tag; attributes that don't exist in the referred tag should be
* appended, and attributes that DO exist should be overwritten.
*/
var sRefAttrs = matchRef[3];
if (sRefAttrs) {
var aXMLRefTag = sXMLRef.match(new RegExp("<" + matchRef[1] + "[^>]*>"));
if (aXMLRefTag) {
var sXMLNewTag = aXMLRefTag[0];
/*
* Iterate over all the attributes in the "referring" XML tag (sRefAttrs)
*/
var matchAttr;
var reAttr = /( [a-z]+=)(['"])(.*?)\2/gi;
while ((matchAttr = reAttr.exec(sRefAttrs))) {
if (sXMLNewTag.toLowerCase().indexOf(matchAttr[1].toLowerCase()) < 0) {
/*
* This is the append case....
*/
sXMLNewTag = sXMLNewTag.replace(">", matchAttr[0] + ">");
} else {
/*
* This is the overwrite case....
*/
sXMLNewTag = sXMLNewTag.replace(new RegExp(matchAttr[1] + "(['\"])(.*?)\\1"), matchAttr[0]);
}
}
if (aXMLRefTag[0] != sXMLNewTag) {
sXMLRef = sXMLRef.replace(aXMLRefTag[0], sXMLNewTag);
}
} else {
done(sXML, "missing <" + matchRef[1] + "> in " + sRefFile);
return;
}
}
/*
* Apparently when a Windows Azure server delivers one of my XML files, it may modify the first line:
*
* <?xml version="1.0" encoding="UTF-8"?>\n
*
* I didn't determine exactly what it was doing at this point (probably just changing the \n to \r\n),
* but in any case, relaxing the following replace() solved it.
*/
sXMLRef = sXMLRef.replace(/<\?xml[^>]*>[\r\n]*/, "");
sXML = sXML.replace(matchRef[0], sXMLRef);
resolveXML(sXML, display, done);
};
display("Loading " + sRefFile + "...");
Web.getResource(sRefFile, null, fAsync, doneReadXML);
return;
}
done(sXML, null);
}
/**
* embedMachine(sAppName, sAppClass, sVersion, idMachine, sXMLFile, sXSLFile, sParms)
*
* This allows to you embed a machine on a web page, by transforming the machine XML into HTML.
*
* @param {string} sAppName is the app name (eg, "PCx86")
* @param {string} sAppClass is the app class (eg, "pcx86"); also known as the machine class
* @param {string} sVersion is the app version (eg, "1.15.7")
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedMachine(sAppName, sAppClass, sVersion, idMachine, sXMLFile, sXSLFile, sParms)
{
var eMachine, eWarning, fSuccess = true;
cAsyncMachines++;
Component.addMachine(idMachine);
var doneMachine = function() {
if (!--cAsyncMachines) {
if (fAsync) Web.enablePageEvents(true);
}
};
var displayError = function(sError) {
Component.log(sError);
displayMessage("Error: " + sError);
if (fSuccess) doneMachine();
fSuccess = false;
};
var displayMessage = function(sMessage) {
if (eWarning === undefined) {
/*
* Our MarkOut module (in convertMDMachineLinks()) creates machine containers that look like:
*
* <div id="' + sMachineID + '" class="machine-placeholder"><p>Embedded PC</p><p class="machine-warning">...</p></div>
*
* with the "machine-warning" paragraph pre-populated with a warning message that the user will
* see if nothing at all happens. But hopefully, in the normal case (and especially the error case),
* *something* will have happened.
*
* Note that it is the HTMLOut module (in processMachines()) that ultimately decides which scripts to
* include and then generates the embedXXX() call.
*/
var aeWarning = (eMachine && Component.getElementsByClass(eMachine, "machine-warning"));
eWarning = (aeWarning && aeWarning[0]) || eMachine;
}
if (eWarning) eWarning.innerHTML = Str.escapeHTML(sMessage);
};
try {
eMachine = document.getElementById(idMachine);
if (eMachine) {
/*
* If we have a 'css' resource, add it to the page first.
*/
var css;
if (typeof resources == "object" && (css = resources['css'])) {
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
head.appendChild(style);
}
if (!sXSLFile) {
/*
* Now that PCjs is an open-source project, we can make the following test more flexible,
* and revert to the internal template if DEBUG *or* internal version (instead of *and*).
*
* Third-party sites that don't use the PCjs server will ALWAYS want to specify a fully-qualified
* path to the XSL file, unless they choose to mirror our folder structure.
*/
var sAppFolder = sAppClass;
if (DEBUG || sVersion == "1.x.x") {
sXSLFile = "/modules/" + sAppFolder + "/templates/components.xsl";
} else {
if (sAppClass.substr(0, 3) == "pdp") sAppFolder = "pdpjs";
sXSLFile = "/versions/" + sAppFolder + "/" + sVersion + "/components.xsl";
}
}
var processXML = function(sXML, xml) {
if (!xml) {
displayError(sXML);
return;
}
/*
* Non-COMPILED kludge to extract the version number from the stylesheet path in the machine XML file;
* we don't need this code in COMPILED (non-DEBUG) releases, because APPVERSION is hard-coded into them.
*/
if (!COMPILED) {
var aMatch = sXML.match(/<\?xml-stylesheet[^>]* href=(['"])[^'"]*?\/([0-9.]*)\/([^'"]*)\1/);
if (aMatch) XMLVERSION = aMatch[2];
}
var transformXML = function(sXSL, xsl) {
if (!xsl) {
displayError(sXSL);
return;
}
/*
* Record the XSL file, in case someone wants to save the entire machine later.
*/
Component.addMachineResource(idMachine, sXSLFile, sXSL);
/*
* The <machine> template in components.xsl now generates a "machine div" that makes
* the div we required the caller of embedMachine() to provide redundant, so instead
* of appending this fragment to the caller's node, we REPLACE the caller's node.
* This works only because because we ALSO inject the caller's "machine div" ID into
* the fragment's ID during parseXML().
*
* eMachine.innerHTML = sFragment;
*
* Also, if the transform function fails, make sure you're using the appropriate
* "components.xsl" and not a "machine.xsl", because the latter will not produce valid
* embeddable HTML (and is the most common cause of failure at this final stage).
*/
displayMessage("Processing " + sXMLFile + "...");
/*
* Beginning with Microsoft Edge and the corresponding release of Windows 10, all the
* 'ActiveXObject' crud has gone away; but of course, this code must remain in place if
* we want to continue supporting older Internet Explorer browsers (ie, back to IE9).
*/
if (window.ActiveXObject || 'ActiveXObject' in window) { // second test is required for IE11 on Windows 8.1
var sFragment = xml['transformNode'](xsl);
if (sFragment) {
eMachine.outerHTML = sFragment;
doneMachine();
} else {
displayError("transformNodeToObject failed");
}
}
else if (document.implementation && document.implementation.createDocument) {
var xsltProcessor = new XSLTProcessor();
xsltProcessor['importStylesheet'](xsl);
var eFragment = xsltProcessor['transformToFragment'](xml, document);
if (eFragment) {
/*
* This fails in Microsoft Edge...
*
var machine = eFragment.getElementById(idMachine);
if (!machine) {
displayError("machine generation failed: " + idMachine);
} else
*/
if (eMachine.parentNode) {
eMachine.parentNode.replaceChild(eFragment, eMachine);
doneMachine();
} else {
/*
* NOTE: This error can occur if our Node web server, when processing a folder with
* both a manifest.xml with a machine.xml reference AND a README.md containing a
* machine link, generates duplicate embedXXX() calls for the same machine; if the
* first embedXXX() call finds its target, subsequent calls for the same target will
* fail.
*
* Technically, such a folder is in a misconfigured state, but it happens, in part
* because when we switched to the Jekyll web server, we had to add machine links to
* all README.md files where we had previously relied on manifest.xml or machine.xml
* processing. This is because the Jekyll web server currently doesn't process XML
* files, nor is support for that likely to be added any time soon; it was a nice
* feature of the Node web server, but it's not clear that it's worth doing for Jekyll.
*/
displayError("invalid machine element: " + idMachine);
}
} else {
displayError("transformToFragment failed");
}
} else {
/*
* Perhaps I should have performed this test at the outset; on the other hand, I'm
* not aware of any browsers don't support one or both of the above XSLT transformation
* methods, so treat this as a bug.
*/
displayError("unable to transform XML: unsupported browser");
}
};
loadXML(sXSLFile, null, sAppName, sAppClass, null, false, displayMessage, transformXML);
};
if (sXMLFile.charAt(0) != '<') {
loadXML(sXMLFile, idMachine, sAppName, sAppClass, sParms, true, displayMessage, processXML);
} else {
parseXML(sXMLFile, null, idMachine, sAppName, sAppClass, sParms, false, displayMessage, processXML);
}
} else {
displayError("missing machine element: " + idMachine);
}
} catch(e) {
displayError(e.message);
}
return fSuccess;
}
/**
* embedC1P(idMachine, sXMLFile, sXSLFile)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @return {boolean} true if successful, false if error
*/
function embedC1P(idMachine, sXMLFile, sXSLFile)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("C1Pjs", "c1pjs", APPVERSION, idMachine, sXMLFile, sXSLFile);
}
/**
* embedPCx86(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPCx86(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PCx86", "pcx86", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* embedPC8080(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPC8080(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PC8080", "pc8080", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* embedPDP10(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPDP10(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PDPjs", "pdp10", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* embedPDP11(idMachine, sXMLFile, sXSLFile, sParms)
*
* @param {string} idMachine
* @param {string} sXMLFile
* @param {string} sXSLFile
* @param {string} [sParms]
* @return {boolean} true if successful, false if error
*/
function embedPDP11(idMachine, sXMLFile, sXSLFile, sParms)
{
if (fAsync) Web.enablePageEvents(false);
return embedMachine("PDPjs", "pdp11", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
}
/**
* findMachineComponent(idMachine, sType)
*
* @param {string} idMachine
* @param {string} sType
* @return {Component|null}
*/
function findMachineComponent(idMachine, sType)
{
return Component.getComponentByType(sType, idMachine + ".machine");
}
/**
* processMachineScript(idMachine, sScript)
*
* @param {string} idMachine
* @param {string} sScript
* @return {boolean}
*/
function processMachineScript(idMachine, sScript)
{
return Component.processScript(idMachine, sScript);
}
/**
* Prevent the Closure Compiler from renaming functions we want to export, by adding them as global properties.
*
* TODO: Consider making all these functions properties on a single global object (eg, 'PCjs'), to minimize global
* pollution and risk of name collision.
*/
if (APPNAME == "C1Pjs") {
window['embedC1P'] = embedC1P;
}
if (APPNAME == "PCx86") {
window['embedPC'] = embedPCx86; // WARNING: embedPC() deprecated as of v1.23.0
window['embedPCx86'] = embedPCx86;
}
if (APPNAME == "PC8080") {
window['embedPC8080'] = embedPC8080;
}
if (APPNAME == "PDPjs") {
window['embedPDP10'] = embedPDP10;
window['embedPDP11'] = embedPDP11;
}
window['findMachineComponent'] = findMachineComponent;
window['processMachineScript'] = processMachineScript;
window['enableEvents'] = Web.enablePageEvents;
window['sendEvent'] = Web.sendPageEvent;