}
+ */
+ 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 ...
tags, and eliminate any HTML entities.
+ */
+ sText = "";
+ var match, re = /([\s\S]*?)<\/pre>/gi;
+ while (match = re.exec(sResource)) {
+ 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) 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.}
+ */
+ 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.} [aParms]
+ * @param {Array.} [aValues]
+ * @param {Array.} [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.} [aParms]
+ * @param {Array.} [aValues]
+ * @param {Array.} [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.