More MACRO-10 support

This commit is contained in:
Jeff 2017-03-11 23:30:06 -08:00 committed by Jeff Parsons
commit e2e018af67
8 changed files with 706 additions and 205 deletions

View file

@ -91,9 +91,11 @@ class DebuggerPDP10 extends Debugger {
* Since this Debugger doesn't use replaceRegs(), we can use parentheses instead of braces.
*/
this.fInit = false;
this.fParens = true;
this.nBusWidth = 18; // default value, updated by initBus()
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.

View file

@ -36,8 +36,33 @@ if (NODE) {
var DebuggerPDP10 = require("./debugger");
}
/**
* @typedef {{
* name:(string),
* nOperand:(number),
* aParms:(string),
* sText:(string)
* }}
*/
var Macro;
/**
* @typedef {{
* name:(string),
* value:(number),
* fLabel:(boolean),
* fGlobal:(boolean),
* fPrivate:(boolean)
* }}
*/
var Sym;
/**
* @class Macro10
* @property {string} sURL
* @property {number} nAddr
* @property {DebuggerPDP10} dbg
* @property {function(Macro10,string,number)} done
*/
class Macro10 {
/**
@ -64,13 +89,14 @@ class Macro10 {
constructor(sURL, nAddr, dbg, done)
{
this.sURL = sURL;
this.nAddr = nAddr;
this.nAddr = nAddr || 0;
this.dbg = dbg;
this.done = done;
/*
* Set up all the services we need to use.
*/
this.println = dbg && dbg.println || console.log;
this.println = dbg.println;
/*
* Initialize all the tables that MACRO-10 uses.
@ -78,6 +104,29 @@ class Macro10 {
this.tblMacros = {};
this.tblSymbols = {};
this.nLine = 0;
this.nError = 0;
this.aWords = []; // filled in by the various genXXX() functions
this.sOperator = null; // the active operator, if any
this.nMacroDef = 0; // the active MACRO definition state
this.sMacroDef = null; // the active MACRO definition name
/*
* 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 = "";
var macro10 = this;
Web.getResource(sURL, null, true, function(sURL, sResource, nErrorCode) {
if (!nErrorCode) {
@ -99,47 +148,75 @@ class Macro10 {
*/
parseFile(sPath, sContents)
{
var sText = sContents;
if (Str.endsWith(sPath, ".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(sContents)) {
var s = match[1];
if (s.indexOf('&') >= 0) s = s.replace(/&lt;/gi, '<').replace(/&gt;/gi, '>').replace(/&amp;/gi, '&');
sText += s;
var a = this.dbg.resetVariables();
try {
var sText = sContents;
if (Str.endsWith(sPath, ".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(sContents)) {
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) this.warning("unrecognized HTML entity: " + match[0]);
}
match = sText.match(/&[a-z]+;/i);
if (match) this.warning("unrecognized HTML entity: " + match[0]);
var i;
var asLines = sText.split(/\r?\n/);
for (i = 0; i < asLines.length; i++) {
this.nLine++;
if (!this.parseLine(asLines[i] + '\r\n')) break;
}
} catch(err) {
this.println(err.message);
this.nError = -1;
}
var i;
var asLines = sText.split(/\r?\n/);
for (i = 0; i < asLines.length; i++) {
if (!this.parseLine(i + 1, asLines[i])) break;
}
return 0;
this.dbg.restoreVariables(a);
return this.nError;
}
/**
* parseLine(nLine, sLine)
* parseLine(sLine)
*
* @this {Macro10}
* @param {number} nLine (line number)
* @param {string} sLine (line contents)
* @return {boolean}
*/
parseLine(nLine, sLine)
parseLine(sLine)
{
var reLine = /([A-Z$%.][0-9A-Z$%.]*[:=]|)\s*([^\s;]+|)\s*([^;]+|)\s*(;?.*)/i;
if (this.nMacroDef) {
if (this.nMacroDef == 1) {
var i = sLine.indexOf('<');
if (i >= 0) {
this.nMacroDef++;
sLine = sLine.substr(i+1);
} else {
this.error("expected " + this.sOperator + " definition: " + sLine);
}
}
if (this.nMacroDef > 1) {
sLine = this.appendMacro(sLine);
}
if (this.nMacroDef) return true;
}
if (this.chASCII != null) {
sLine = this.addASCII(sLine);
}
var reLine = /\s*([A-Z$%.][0-9A-Z$%.]*[:=]|)\s*([A-Z$%.][0-9A-Z$%.]*|)\s*([^;]+|)\s*(;?.*)/i;
var match = sLine.match(reLine);
if (!match || match[4] && match[4].slice(0, 1) != ';') {
this.warning("failed to parse line " + nLine + ": " + sLine);
this.error("failed to parse line: " + sLine);
return false;
}
var sLabel = match[1];
var sOperator = match[2];
var sOperator = match[2].toUpperCase();
var sOperands = match[3].trim();
var sComment = match[4].slice(1);
if (sLabel) {
@ -148,14 +225,103 @@ class Macro10 {
if (chSep == ':') {
this.addLabel(sLabel);
} else {
this.addVariable(sLabel, sOperands = sOperator + sOperands);
sOperands = sOperator + sOperands;
sOperator = chSep;
}
}
if (DEBUG) this.println(Str.toDec(nLine, 5) + ": label(" + sLabel + ") operator(" + sOperator + ") operands(" + sOperands + ") comment(" + sComment + ")");
this.sOperator = sOperator;
sOperands = sOperands.trim();
switch(sOperator) {
case "":
break; // eg, a blank line or a line that contains only a label and/or a comment
case "=":
this.addAssign(sLabel, sOperands);
break;
case Macro10.PSEUDO_OP.ASCII:
case Macro10.PSEUDO_OP.ASCIZ:
case Macro10.PSEUDO_OP.SIXBIT:
this.addASCII(sOperands);
break;
case Macro10.PSEUDO_OP.DEFINE:
case Macro10.PSEUDO_OP.IFE:
case Macro10.PSEUDO_OP.REPEAT:
this.addMacro(sOperator, sOperands);
break;
case Macro10.PSEUDO_OP.PAGE: // TODO
case Macro10.PSEUDO_OP.SUBTTL: // TODO
break;
default:
if (DEBUG) this.println(Str.toDec(this.nLine, 5) + ": label(" + sLabel + ") operator(" + sOperator + ") operands(" + sOperands + ") comment(" + sComment + ")");
break;
}
if (this.nLine >= 330) {
this.dbg.printVariable();
this.error("temporary line limit reached");
return false;
}
return true;
}
/**
* parseMacro()
*
* @this {Macro10}
*/
parseMacro()
{
if (!this.sMacroDef) return;
var macro = this.tblMacros[this.sMacroDef];
var sOperator = this.sMacroDef[0] == '@'? this.sMacroDef.substr(1) : this.sOperator;
switch(sOperator) {
case Macro10.PSEUDO_OP.IFE:
if (!macro.nOperand) {
this.parseText(macro.sText);
}
break;
case Macro10.PSEUDO_OP.REPEAT:
while (macro.nOperand-- > 0) {
this.parseText(macro.sText);
}
break;
}
}
/**
* parseText(sText)
*
* @this {Macro10}
* @param {string} sText
*/
parseText(sText)
{
var asLines = sText.split(/\r?\n/);
for (var i = 0; i < asLines.length; i++) {
if (!this.parseLine(asLines[i] + '\r\n')) break;
}
}
/**
* error(sError)
*
* @this {Macro10}
* @param {string} sError
*/
error(sError)
{
throw new Error("error" + (this.nLine? " at line " + Str.toDec(this.nLine) : "") + ": " + sError);
}
/**
* warning(sWarning)
*
@ -164,7 +330,90 @@ class Macro10 {
*/
warning(sWarning)
{
this.println("warning: " + sWarning);
this.println("warning" + (this.nLine? " at line " + Str.toDec(this.nLine) : "") + ": " + sWarning);
}
/**
* getExpression(sInput, chDelim)
*
* @this {Macro10}
* @param {string} sInput
* @param {string} [chDelim]
* @return {string|null} (if the input string begins with an expression, return it)
*/
getExpression(sInput, chDelim)
{
var sExp = null;
var cNesting = 0;
for (var i = 0; i < sInput.length; i++) {
var ch = sInput[i];
if (ch == chDelim) {
if (!cNesting) {
sExp = sInput.substr(0, i);
break;
}
}
if (ch == '<') {
cNesting++;
} else if (ch == '>') {
if (--cNesting < 0) {
this.error("missing angle bracket(s): " + sInput);
break;
}
}
}
return sExp;
}
/**
* addASCII(sOperands)
*
* @this {Macro10}
* @param {string} sOperands
* @return {string} (returns whatever portion of the string was not part of an ASCII pseudo-op)
*/
addASCII(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;
}
/**
* addAssign(sName, sExp)
*
* @this {Macro10}
* @param {string} sName
* @param {string} sExp
*/
addAssign(sName, sExp)
{
var value = this.dbg.parseExpression(sExp);
if (value === undefined) {
this.error("parseExpression(" + sExp + ")");
return;
}
this.addSymbol(sName, value);
}
/**
@ -175,16 +424,200 @@ class Macro10 {
*/
addLabel(sLabel)
{
this.addSymbol(sLabel, this.nAddr, true);
}
/**
* addVariable(sVar, sExp)
* addMacro(sOperator, sOperands)
*
* If sOperator is DEFINE, 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 (-1 for regular macros).
*
* 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} sVar
* @param {string} sExp
* @param {string} sOperator
* @param {string} sOperands
*/
addVariable(sVar, sExp)
addMacro(sOperator, sOperands)
{
var match, name, aParms, nOperand, iDelim;
if (sOperator == Macro10.PSEUDO_OP.DEFINE) {
match = sOperands.match(/([A-Z$%.][0-9A-Z$%.]*)\s*(\([^)]*\)|)\s*(<|)(.*)/i);
if (!match) {
this.error("unrecognized " + sOperator + " definition: " + sOperands);
return;
}
/*
* TODO: Tighten up this parsing at some point. All this is doing is extracting entire symbols
* from within the parentheses, if any; it's NOT ensuring that those symbols are comma-separated
* with no other intervening characters.
*/
name = match[1];
aParms = match[2].match(/[A-Z$%.][0-9A-Z$%.]*/g);
nOperand = -1;
iDelim = 3;
} else {
var sExp = this.getExpression(sOperands, ',');
if (!sExp) {
this.error("missing " + sOperator + " expression: " + sOperands);
return;
}
sOperands = sOperands.substr(sExp.length + 1);
sExp = sExp.trim();
match = sOperands.match(/\s*(<|)(.*)/i);
name = '@' + sOperator;
aParms = [];
nOperand = this.dbg.parseExpression(sExp);
iDelim = 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).
*/
this.nMacroDef = 1;
this.sMacroDef = name;
var sText = "";
if (match[iDelim]) { // if there IS an angle bracket...
this.nMacroDef = 2; // then the macro definition has begun
sText = match[iDelim + 1];
if (sText.slice(-1) == '>') { // and if there is ALSO a closing angle bracket...
this.nMacroDef = 0; // the macro definition has also ended
sText = sText.slice(0, -1);
}
}
this.tblMacros[name] = {
name: name,
aParms: aParms,
nOperand: nOperand,
sText: sText
};
if (!this.nMacroDef) this.parseMacro();
}
/**
* 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.nMacroDef++;
} else if (sLine[i] == '>') {
this.nMacroDef--;
if (this.nMacroDef == 1) {
this.nMacroDef = 0;
sRemain = sLine.substr(i + 1);
sLine = sLine.substr(0, i);
break;
}
}
}
var macro = this.tblMacros[this.sMacroDef];
macro.sText += sLine;
if (!this.nMacroDef) this.parseMacro();
return sRemain;
}
/**
* addSymbol(name, value, fLabel, fGlobal, fPrivate)
*
* @this {Macro10}
* @param {string} name
* @param {number} value
* @param {boolean} [fLabel] (default is false, meaning the symbol is an assignment)
* @param {boolean} [fGlobal] (default is false, meaning the symbol is local to the current file)
* @param {boolean} [fPrivate] (default is false, meaning the symbol has visibility to the caller)
*/
addSymbol(name, value, fLabel = false, fGlobal = false, fPrivate = false)
{
name = name.toUpperCase().substr(0, 6);
if (fLabel && this.tblSymbols[name] !== undefined) {
this.error("label " + name + " redefined");
return;
}
this.tblSymbols[name] = {
name: name,
value: value,
fLabel: fLabel,
fGlobal: fGlobal,
fPrivate: fPrivate
};
this.dbg.setVariable(name, value);
}
/**
* genASCII()
*
* Based on the last operator, generate the appropriate ASCII 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;
w += c * Math.pow(2, shift);
shift -= bits;
n++;
if (shift < 0) {
this.genWord(w);
n = 0;
}
}
if (n) this.genWord(w);
}
/**
* genWord(w)
*
* @this {Macro10}
* @param {number} w
*/
genWord(w)
{
this.aWords[this.nAddr++] = w;
}
}
Macro10.PSEUDO_OP = {
ASCII: "ASCII",
ASCIZ: "ASCIZ",
SIXBIT: "SIXBIT",
DEFINE: "DEFINE",
IFE: "IFE",
REPEAT: "REPEAT",
PAGE: "PAGE",
SUBTTL: "SUBTTL",
};

View file

@ -89,7 +89,9 @@ class DebuggerPDP11 extends Debugger {
* Since this Debugger doesn't use replaceRegs(), we can use parentheses instead of braces.
*/
this.fInit = false;
this.fParens = true;
this.achGroup = ['(',')'];
this.achAddress = [];
/*
* Most commands that require an address call parseAddr(), which defaults to dbgAddrNextCode

View file

@ -100,7 +100,9 @@ class Debugger extends Component {
* Default base used to display all values; modified with the "s base" command.
*/
this.nBase = +parmsDbg['base'] || 16;
this.fParens = false;
this.achGroup = ['{','}'];
this.achAddress = ['[',']'];
/*
* These keep track of instruction activity, but only when tracing or when Debugger checks
@ -409,19 +411,19 @@ class Debugger extends Component {
* ...
*
* 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.
* predecessor is encountered, evaluate, and push the result back onto aVals. Unary operators like
* '~' and ternary operators like '?:' are not supported.
*
* Unary operators like '~' and ternary operators like '?:' are not supported; neither are parentheses.
* parseReference() makes it possible to write parenthetical-style sub-expressions by using whatever
* characters achGroup contains (default is braces}. Address references are resolved using the characters
* in achAddress (default is brackets).
*
* However, parseReference() now makes it possible to write parenthetical-style sub-expressions by using
* {...} (braces), as well as address references by using [...] (brackets).
* Why not always use parentheses for sub-expressions? Because parseReference() serves multiple purposes,
* the other being reference replacement in message strings passing through replaceRegs(), and some
* Debuggers don't want parentheses taking on a new meaning in message strings.
*
* Why am I using braces instead of parentheses for sub-expressions? Because parseReference() serves
* multiple purposes, the other being reference replacement in message strings passing through replaceRegs(),
* and I didn't want parentheses taking on a new meaning in message strings.
*
* However, a Debugger can override this choice by setting fParens to true, if there's no conflict in its
* replaceRegs() implementation.
* However, a Debugger can override these choices by modifying achGroup and/or achAddress, if there's no
* conflict in its replaceRegs() implementation.
*
* @this {Debugger}
* @param {string|undefined} sExp
@ -494,8 +496,7 @@ class Debugger extends Component {
*
* 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. Owing to this function's simplistic parsing, nested braces/brackets are not supported
* (define intermediate variables if needed).
* addresses.
*
* @this {Debugger}
* @param {string} s
@ -503,17 +504,36 @@ class Debugger extends Component {
*/
parseReference(s) {
var a;
var chOpen = this.fParens? '(' : '{';
var chClose = this.fParens? ')' : '}';
var reSubExp = new RegExp(this.fParens? "\\((.*?)\\)" : "\\{(.*?)\\}");
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)) {
if (a[1].indexOf(chOpen) >= 0) break; // unsupported nested brace(s)
var value = this.parseExpression(a[1]);
s = s.replace(chOpen + a[1] + chClose, value != null? this.toStrBase(value) : "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);
}
while (a = s.match(/\[(.*?)]/)) {
if (a[1].indexOf('[') >= 0) break; // unsupported nested bracket(s)
s = this.parseAddrReference(s, a[1]);
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);
}
@ -595,6 +615,28 @@ class Debugger extends Component {
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)
*

View file

@ -65,6 +65,10 @@ class Str {
* 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. If this support turns out to adversely affect
* other debuggers, then it will have to be "conditionalized".
*
* To summarize our non-standard alternatives: a 'y' suffix indicates binary, a '#' prefix indicates
* octal, a '$' prefix indicates hex, and a "0b" prefix indicates binary IF at least one comma is present.
* Commas are useful for grouping binary digits, but if you don't want to use them, then you must use a
@ -110,6 +114,21 @@ class Str {
chPrefix = null;
}
}
else if (chPrefix == '^') {
chPrefix = s.charAt(1);
if (chPrefix == 'D') {
base = 10;
chPrefix = null;
}
else if (chPrefix == 'O') {
base = 8;
chPrefix = null;
}
else if (chPrefix == 'B') {
base = 2;
chPrefix = null;
}
}
if (chPrefix == null) {
s = s.substr(2);
}