More MACRO-10 support
This commit is contained in:
parent
b371f9d649
commit
e2e018af67
8 changed files with 706 additions and 205 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(/</gi, '<').replace(/>/gi, '>').replace(/&/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(/</gi, '<').replace(/>/gi, '>').replace(/&/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",
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue