/* * Copyright 2019 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Text; using System.Web.Script.Serialization; using Asm65; using CommonUtil; namespace SourceGen { /// /// Data pseudo-op formatter. Long operands, notably strings and dense hex blocks, may /// be broken across multiple lines. /// /// Assembler output will use Opcode and Operand, emitting multiple lines of ASC, HEX, /// etc. The display list may treat it as a single item that is split across /// multiple lines. /// public class PseudoOp { private const int MAX_OPERAND_LEN = 64; /// /// One piece of the pseudo-instruction. /// public struct PseudoOut { /// /// Opcode. Same for all entries in the list. /// public string Opcode { get; set; } /// /// Formatted form of this piece of the operand. /// public string Operand { get; set; } /// /// Copy constructor. /// public PseudoOut(PseudoOut src) { Opcode = src.Opcode; Operand = src.Operand; } } #region PseudoOpNames /// /// Pseudo-op name collection. Instances are immutable. /// public class PseudoOpNames { public string EquDirective { get; private set; } public string VarDirective { get; private set; } public string OrgDirective { get; private set; } public string RegWidthDirective { get; private set; } public string DefineData1 { get; private set; } public string DefineData2 { get; private set; } public string DefineData3 { get; private set; } public string DefineData4 { get; private set; } public string DefineBigData2 { get; private set; } public string DefineBigData3 { get; private set; } public string DefineBigData4 { get; private set; } public string Fill { get; private set; } public string Dense { get; private set; } public string StrGeneric { get; private set; } public string StrReverse { get; private set; } public string StrLen8 { get; private set; } public string StrLen16 { get; private set; } public string StrNullTerm { get; private set; } public string StrDci { get; private set; } /// /// Constructs an empty PseudoOp, for deserialization. /// public PseudoOpNames() : this(new Dictionary()) { } /// /// Constructor. Pass in a dictionary with name/value pairs. Unknown names /// will be ignored, missing names will be assigned the empty string. /// /// Dictionary of values. public PseudoOpNames(Dictionary dict) { foreach (PropertyInfo prop in GetType().GetProperties()) { dict.TryGetValue(prop.Name, out string value); if (value == null) { value = string.Empty; } prop.SetValue(this, value); } } public static bool operator ==(PseudoOpNames a, PseudoOpNames b) { if (ReferenceEquals(a, b)) { return true; // same object, or both null } if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) { return false; // one is null } return a.EquDirective == b.EquDirective && a.VarDirective == b.VarDirective && a.OrgDirective == b.OrgDirective && a.RegWidthDirective == b.RegWidthDirective && a.DefineData1 == b.DefineData1 && a.DefineData2 == b.DefineData2 && a.DefineData3 == b.DefineData3 && a.DefineData4 == b.DefineData4 && a.DefineBigData2 == b.DefineBigData2 && a.DefineBigData3 == b.DefineBigData3 && a.DefineBigData4 == b.DefineBigData4 && a.Fill == b.Fill && a.Dense == b.Dense && a.StrGeneric == b.StrGeneric && a.StrReverse == b.StrReverse && a.StrLen8 == b.StrLen8 && a.StrLen16 == b.StrLen16 && a.StrNullTerm == b.StrNullTerm && a.StrDci == b.StrDci; } public static bool operator !=(PseudoOpNames a, PseudoOpNames b) { return !(a == b); } public override bool Equals(object obj) { return obj is PseudoOpNames && this == (PseudoOpNames)obj; } public override int GetHashCode() { // should be enough return (EquDirective == null ? 0 : EquDirective.GetHashCode()) ^ (OrgDirective == null ? 0 : OrgDirective.GetHashCode()) ^ (DefineData1 == null ? 0 : DefineData1.GetHashCode()) ^ (Fill == null ? 0 : Fill.GetHashCode()); } public string GetDefineData(int width) { switch (width) { case 1: return DefineData1; case 2: return DefineData2; case 3: return DefineData3; case 4: return DefineData4; default: Debug.Assert(false); return ".?!!"; } } public string GetDefineBigData(int width) { switch (width) { case 1: return DefineData1; case 2: return DefineBigData2; case 3: return DefineBigData3; case 4: return DefineBigData4; default: Debug.Assert(false); return ".!!?"; } } /// /// Merges the non-null, non-empty strings. /// public static PseudoOpNames Merge(PseudoOpNames basePon, PseudoOpNames newPon) { Dictionary baseDict = PropsToDict(basePon); Dictionary newDict = PropsToDict(newPon); foreach (KeyValuePair kvp in newDict) { if (string.IsNullOrEmpty(kvp.Value)) { continue; } baseDict[kvp.Key] = kvp.Value; } return new PseudoOpNames(baseDict); } private static Dictionary PropsToDict(PseudoOpNames pon) { Dictionary dict = new Dictionary(); foreach (PropertyInfo prop in pon.GetType().GetProperties()) { string value = (string)prop.GetValue(pon); if (!string.IsNullOrEmpty(value)) { dict[prop.Name] = value; } } return dict; } public string Serialize() { // This results in a JSON-encoded string being stored in a JSON-encoded file, // which means a lot of double-quote escaping. We could do something here // that stored more nicely but it doesn't seem worth the effort. JavaScriptSerializer ser = new JavaScriptSerializer(); Dictionary dict = PropsToDict(this); return ser.Serialize(dict); } public static PseudoOpNames Deserialize(string cereal) { JavaScriptSerializer ser = new JavaScriptSerializer(); try { Dictionary dict = ser.Deserialize>(cereal); return new PseudoOpNames(dict); } catch (Exception ex) { Debug.WriteLine("PseudoOpNames deserialization failed: " + ex.Message); return new PseudoOpNames(); } } } /// /// Returns a PseudoOpNames instance with some reasonable defaults for on-screen display. /// public static PseudoOpNames DefaultPseudoOpNames { get; } = new PseudoOpNames(new Dictionary { { "EquDirective", ".eq" }, { "VarDirective", ".var" }, { "OrgDirective", ".org" }, { "RegWidthDirective", ".rwid" }, { "DefineData1", ".dd1" }, { "DefineData2", ".dd2" }, { "DefineData3", ".dd3" }, { "DefineData4", ".dd4" }, { "DefineBigData2", ".dbd2" }, { "DefineBigData3", ".dbd3" }, { "DefineBigData4", ".dbd4" }, { "Fill", ".fill" }, { "Dense", ".bulk" }, { "StrGeneric", ".str" }, { "StrReverse", ".rstr" }, { "StrLen8", ".l1str" }, { "StrLen16", ".l2str" }, { "StrNullTerm", ".zstr" }, { "StrDci", ".dstr" } }); #endregion PseudoOpNames /// /// Computes the number of lines of output required to hold the formatted output. /// /// Format definition. /// Data format descriptor. /// Line count. public static int ComputeRequiredLineCount(Formatter formatter, PseudoOpNames opNames, FormatDescriptor dfd, byte[] data, int offset) { if (dfd.IsString) { Debug.Assert(false); // shouldn't be calling here anymore List lines = FormatStringOp(formatter, opNames, dfd, data, offset, out string popcode); return lines.Count; } switch (dfd.FormatType) { case FormatDescriptor.Type.Default: case FormatDescriptor.Type.NumericLE: case FormatDescriptor.Type.NumericBE: case FormatDescriptor.Type.Fill: return 1; case FormatDescriptor.Type.Dense: { // no delimiter, two output bytes per input byte int maxLen = MAX_OPERAND_LEN; int textLen = dfd.Length * 2; return (textLen + maxLen - 1) / maxLen; } default: Debug.Assert(false); return 1; } } /// /// Generates a pseudo-op statement for the specified data operation. /// /// For most operations, only one output line will be generated. For larger items, /// like dense hex, the value may be split into multiple lines. The sub-index /// indicates which line should be formatted. /// /// Format definition. /// Table of pseudo-op names. /// Project symbol table. /// Symbol label map. May be null. /// Data format descriptor. /// File data array. /// Start offset. /// For multi-line items, which line. public static PseudoOut FormatDataOp(Formatter formatter, PseudoOpNames opNames, SymbolTable symbolTable, Dictionary labelMap, FormatDescriptor dfd, byte[] data, int offset, int subIndex) { if (dfd == null) { // should never happen //Debug.Assert(false, "Null dfd at offset+" + offset.ToString("x6")); PseudoOut failed = new PseudoOut(); failed.Opcode = failed.Operand = "!FAILED!+" + offset.ToString("x6"); return failed; } int length = dfd.Length; Debug.Assert(length > 0); // All outputs for a given offset show the same offset and length, even for // multi-line items. PseudoOut po = new PseudoOut(); if (dfd.IsString) { Debug.Assert(false); // shouldn't be calling here anymore List lines = FormatStringOp(formatter, opNames, dfd, data, offset, out string popcode); po.Opcode = popcode; po.Operand = lines[subIndex]; } else { switch (dfd.FormatType) { case FormatDescriptor.Type.Default: if (length != 1) { // This shouldn't happen. Debug.Assert(false); length = 1; } po.Opcode = opNames.GetDefineData(length); int operand = RawData.GetWord(data, offset, length, false); po.Operand = formatter.FormatHexValue(operand, length * 2); break; case FormatDescriptor.Type.NumericLE: po.Opcode = opNames.GetDefineData(length); operand = RawData.GetWord(data, offset, length, false); po.Operand = FormatNumericOperand(formatter, symbolTable, labelMap, dfd, operand, length, FormatNumericOpFlags.None); break; case FormatDescriptor.Type.NumericBE: po.Opcode = opNames.GetDefineBigData(length); operand = RawData.GetWord(data, offset, length, true); po.Operand = FormatNumericOperand(formatter, symbolTable, labelMap, dfd, operand, length, FormatNumericOpFlags.None); break; case FormatDescriptor.Type.Fill: po.Opcode = opNames.Fill; po.Operand = length + "," + formatter.FormatHexValue(data[offset], 2); break; case FormatDescriptor.Type.Dense: { int maxPerLine = MAX_OPERAND_LEN / 2; offset += subIndex * maxPerLine; length -= subIndex * maxPerLine; if (length > maxPerLine) { length = maxPerLine; } po.Opcode = opNames.Dense; po.Operand = formatter.FormatDenseHex(data, offset, length); //List outList = new List(); //GenerateTextLines(text, "", "", po, outList); //po = outList[subIndex]; } break; default: Debug.Assert(false); po.Opcode = ".???"; po.Operand = "$" + data[offset].ToString("x2"); break; } } return po; } /// /// Converts a collection of bytes that represent a string into an array of formatted /// string operands. /// /// Formatter object. /// Pseudo-opcode name table. /// Format descriptor. /// File data. /// Offset, within data, of start of string. /// Pseudo-opcode string. /// Array of operand strings. public static List FormatStringOp(Formatter formatter, PseudoOpNames opNames, FormatDescriptor dfd, byte[] data, int offset, out string popcode) { int hiddenLeadingBytes = 0; int trailingBytes = 0; StringOpFormatter.ReverseMode revMode = StringOpFormatter.ReverseMode.Forward; Formatter.DelimiterSet delSet = formatter.Config.mStringDelimiters; Formatter.DelimiterDef delDef; CharEncoding.Convert charConv; switch (dfd.FormatSubType) { case FormatDescriptor.SubType.Ascii: if (dfd.FormatType == FormatDescriptor.Type.StringDci) { charConv = CharEncoding.ConvertLowAndHighAscii; } else { charConv = CharEncoding.ConvertAscii; } delDef = delSet.Get(CharEncoding.Encoding.Ascii); break; case FormatDescriptor.SubType.HighAscii: if (dfd.FormatType == FormatDescriptor.Type.StringDci) { charConv = CharEncoding.ConvertLowAndHighAscii; } else { charConv = CharEncoding.ConvertHighAscii; } delDef = delSet.Get(CharEncoding.Encoding.HighAscii); break; case FormatDescriptor.SubType.C64Petscii: if (dfd.FormatType == FormatDescriptor.Type.StringDci) { charConv = CharEncoding.ConvertLowAndHighC64Petscii; } else { charConv = CharEncoding.ConvertC64Petscii; } delDef = delSet.Get(CharEncoding.Encoding.C64Petscii); break; case FormatDescriptor.SubType.C64Screen: if (dfd.FormatType == FormatDescriptor.Type.StringDci) { charConv = CharEncoding.ConvertLowAndHighC64ScreenCode; } else { charConv = CharEncoding.ConvertC64ScreenCode; } delDef = delSet.Get(CharEncoding.Encoding.C64ScreenCode); break; default: Debug.Assert(false); charConv = CharEncoding.ConvertAscii; delDef = delSet.Get(CharEncoding.Encoding.Ascii); break; } if (delDef == null) { delDef = Formatter.DOUBLE_QUOTE_DELIM; } switch (dfd.FormatType) { case FormatDescriptor.Type.StringGeneric: // Generic character data. popcode = opNames.StrGeneric; break; case FormatDescriptor.Type.StringReverse: // Character data, full width specified by formatter. Show characters // in reverse order. popcode = opNames.StrReverse; revMode = StringOpFormatter.ReverseMode.FullReverse; break; case FormatDescriptor.Type.StringNullTerm: // Character data with a terminating null. Don't show the null byte. popcode = opNames.StrNullTerm; trailingBytes = 1; //if (strLen == 0) { // showHexZeroes = 1; //} break; case FormatDescriptor.Type.StringL8: // Character data with a leading length byte. Don't show the length. hiddenLeadingBytes = 1; //if (strLen == 0) { // showHexZeroes = 1; //} popcode = opNames.StrLen8; break; case FormatDescriptor.Type.StringL16: // Character data with a leading length word. Don't show the length. Debug.Assert(dfd.Length > 1); hiddenLeadingBytes = 2; //if (strLen == 0) { // showHexZeroes = 2; //} popcode = opNames.StrLen16; break; case FormatDescriptor.Type.StringDci: // High bit on last byte is flipped. popcode = opNames.StrDci; break; default: Debug.Assert(false); popcode = ".!!!"; break; } StringOpFormatter stropf = new StringOpFormatter(formatter, delDef, StringOpFormatter.RawOutputStyle.CommaSep, MAX_OPERAND_LEN, charConv); stropf.FeedBytes(data, offset + hiddenLeadingBytes, dfd.Length - hiddenLeadingBytes - trailingBytes, 0, revMode); return stropf.Lines; } /// /// Special formatting flags for the FormatNumericOperand() method. /// public enum FormatNumericOpFlags { None = 0, IsPcRel, // opcode is PC relative, e.g. branch or PER HasHashPrefix, // operand has a leading '#', avoiding ambiguity in some cases } /// /// Converts a FormatDescriptor SubType to a CharEncoding.Encoding value. /// /// FormatDescriptor sub-type. /// The corresponding CharEncoding.Encoding value, or Encoding.Unknown /// if the sub-type isn't a character encoding. public static CharEncoding.Encoding SubTypeToEnc(FormatDescriptor.SubType subType) { switch (subType) { case FormatDescriptor.SubType.Ascii: return CharEncoding.Encoding.Ascii; case FormatDescriptor.SubType.HighAscii: return CharEncoding.Encoding.HighAscii; case FormatDescriptor.SubType.C64Petscii: return CharEncoding.Encoding.C64Petscii; case FormatDescriptor.SubType.C64Screen: return CharEncoding.Encoding.C64ScreenCode; default: return CharEncoding.Encoding.Unknown; } } /// /// Format a numeric operand value according to the specified sub-format. /// /// Text formatter. /// Full table of project symbols. /// Symbol label remap, for local label conversion. May be /// null. /// Operand format descriptor. /// Operand's value. For most things this comes directly /// out of the code, for relative branches it's a 24-bit absolute address. /// Length of operand, in bytes. For an instruction, this /// does not include the opcode byte. For a relative branch, this will be 2. /// Special handling. public static string FormatNumericOperand(Formatter formatter, SymbolTable symbolTable, Dictionary labelMap, FormatDescriptor dfd, int operandValue, int operandLen, FormatNumericOpFlags flags) { return FormatNumericOperand(formatter, symbolTable, null, labelMap, dfd, -1, operandValue, operandLen, flags); } /// /// Format a numeric operand value according to the specified sub-format. /// /// Text formatter. /// Full table of project symbols. /// Local variable lookup object. May be null if not /// formatting an instruction. /// Symbol label remap, for local label conversion. May be /// null. /// Operand format descriptor. /// Offset of start of instruction or data pseudo-op, for /// variable name lookup. Okay to pass -1 when not formatting an instruction. /// Operand's value. For most things this comes directly /// out of the code, for relative branches it's a 24-bit absolute address. /// Length of operand, in bytes. For an instruction, this /// does not include the opcode byte. For a relative branch, this will be 2. /// Special handling. public static string FormatNumericOperand(Formatter formatter, SymbolTable symbolTable, LocalVariableLookup lvLookup, Dictionary labelMap, FormatDescriptor dfd, int offset, int operandValue, int operandLen, FormatNumericOpFlags flags) { Debug.Assert(operandLen > 0); int hexMinLen = operandLen * 2; switch (dfd.FormatSubType) { case FormatDescriptor.SubType.None: case FormatDescriptor.SubType.Hex: case FormatDescriptor.SubType.Address: return formatter.FormatHexValue(operandValue, hexMinLen); case FormatDescriptor.SubType.Decimal: return formatter.FormatDecimalValue(operandValue); case FormatDescriptor.SubType.Binary: return formatter.FormatBinaryValue(operandValue, hexMinLen * 4); case FormatDescriptor.SubType.Ascii: case FormatDescriptor.SubType.HighAscii: case FormatDescriptor.SubType.C64Petscii: case FormatDescriptor.SubType.C64Screen: CharEncoding.Encoding enc = SubTypeToEnc(dfd.FormatSubType); return formatter.FormatCharacterValue(operandValue, enc); case FormatDescriptor.SubType.Symbol: if (lvLookup != null && dfd.SymbolRef.IsVariable) { Debug.Assert(operandLen == 1); // only doing 8-bit stuff DefSymbol defSym = lvLookup.GetSymbol(offset, dfd.SymbolRef); if (defSym != null) { StringBuilder sb = new StringBuilder(); FormatNumericSymbolCommon(formatter, defSym, null, dfd, operandValue, operandLen, flags, sb); return sb.ToString(); } else { Debug.WriteLine("Local variable format failed"); Debug.Assert(false); return formatter.FormatHexValue(operandValue, hexMinLen); } } else if (symbolTable.TryGetNonVariableValue(dfd.SymbolRef.Label, out Symbol sym)) { StringBuilder sb = new StringBuilder(); switch (formatter.ExpressionMode) { case Formatter.FormatConfig.ExpressionMode.Common: FormatNumericSymbolCommon(formatter, sym, labelMap, dfd, operandValue, operandLen, flags, sb); break; case Formatter.FormatConfig.ExpressionMode.Cc65: FormatNumericSymbolCc65(formatter, sym, labelMap, dfd, operandValue, operandLen, flags, sb); break; case Formatter.FormatConfig.ExpressionMode.Merlin: FormatNumericSymbolMerlin(formatter, sym, labelMap, dfd, operandValue, operandLen, flags, sb); break; default: Debug.Assert(false, "Unknown expression mode " + formatter.ExpressionMode); return "???"; } return sb.ToString(); } else { return formatter.FormatHexValue(operandValue, hexMinLen); } default: // should not see REMOVE or ASCII_GENERIC here Debug.Assert(false); return "???"; } } /// /// Format the symbol and adjustment using common expression syntax. /// private static void FormatNumericSymbolCommon(Formatter formatter, Symbol sym, Dictionary labelMap, FormatDescriptor dfd, int operandValue, int operandLen, FormatNumericOpFlags flags, StringBuilder sb) { // We could have some simple code that generated correct output, shifting and // masking every time, but that's ugly and annoying. For single-byte ops we can // just use the byte-select operators, for wider ops we get only as fancy as we // need to be. int adjustment, symbolValue; string symLabel = sym.Label; if (labelMap != null && labelMap.TryGetValue(symLabel, out string newLabel)) { symLabel = newLabel; } if (sym.IsVariable) { symLabel = formatter.FormatVariableLabel(symLabel); } if (operandLen == 1) { // Use the byte-selection operator to get the right piece. In 64tass the // selection operator has a very low precedence, similar to Merlin 32. string selOp; if (dfd.SymbolRef.ValuePart == WeakSymbolRef.Part.Bank) { symbolValue = (sym.Value >> 16) & 0xff; if (formatter.Config.mBankSelectBackQuote) { selOp = "`"; } else { selOp = "^"; } } else if (dfd.SymbolRef.ValuePart == WeakSymbolRef.Part.High) { symbolValue = (sym.Value >> 8) & 0xff; selOp = ">"; } else { symbolValue = sym.Value & 0xff; if (symbolValue == sym.Value) { selOp = string.Empty; } else { selOp = "<"; } } operandValue &= 0xff; if (operandValue != symbolValue && dfd.SymbolRef.ValuePart != WeakSymbolRef.Part.Low) { // Adjustment is required to an upper-byte part. sb.Append('('); sb.Append(selOp); sb.Append(symLabel); sb.Append(')'); } else { // no adjustment required sb.Append(selOp); sb.Append(symLabel); } } else if (operandLen <= 4) { // Operands and values should be 8/16/24 bit unsigned quantities. 32-bit // support is really there so you can have a 24-bit pointer in a 32-bit hole. // Might need to adjust this if 32-bit signed quantities become interesting. uint mask = 0xffffffff >> ((4 - operandLen) * 8); int rightShift; if (dfd.SymbolRef.ValuePart == WeakSymbolRef.Part.Bank) { symbolValue = (sym.Value >> 16); rightShift = 16; } else if (dfd.SymbolRef.ValuePart == WeakSymbolRef.Part.High) { symbolValue = (sym.Value >> 8); rightShift = 8; } else { symbolValue = sym.Value; rightShift = 0; } if (flags == FormatNumericOpFlags.IsPcRel) { // PC-relative operands are funny, because an 8- or 16-bit value is always // expanded to 24 bits. We output a 16-bit value that the assembler will // convert back to 8-bit or 16-bit. In any event, the bank byte is never // relevant to our computations. operandValue &= 0xffff; symbolValue &= 0xffff; } bool needMask = false; if (symbolValue > mask) { // Post-shift value won't fit in an operand-size box. symbolValue = (int) (symbolValue & mask); needMask = true; } operandValue = (int)(operandValue & mask); // Generate one of: // label [+ adj] // (label >> rightShift) [+ adj] // (label & mask) [+ adj] // ((label >> rightShift) & mask) [+ adj] if (rightShift != 0 || needMask) { if (flags != FormatNumericOpFlags.HasHashPrefix) { sb.Append("0+"); } if (rightShift != 0 && needMask) { sb.Append("(("); } else { sb.Append("("); } } sb.Append(symLabel); if (rightShift != 0) { sb.Append(" >> "); sb.Append(rightShift.ToString()); sb.Append(')'); } if (needMask) { sb.Append(" & "); sb.Append(formatter.FormatHexValue((int)mask, 2)); sb.Append(')'); } } else { Debug.Assert(false, "bad numeric len"); sb.Append("?????"); symbolValue = 0; } adjustment = operandValue - symbolValue; sb.Append(formatter.FormatAdjustment(adjustment)); } /// /// Format the symbol and adjustment using cc65 expression syntax. /// private static void FormatNumericSymbolCc65(Formatter formatter, Symbol sym, Dictionary labelMap, FormatDescriptor dfd, int operandValue, int operandLen, FormatNumericOpFlags flags, StringBuilder sb) { // The key difference between cc65 and other assemblers with general expressions // is that the bitwise shift and AND operators have higher precedence than the // arithmetic ops like add and subtract. (The bitwise ops are equal to multiply // and divide.) This means that, if we want to mask off the low 16 bits and add one // to a label, we can write "start & $ffff + 1" rather than "(start & $ffff) + 1". // // This is particularly convenient for PEA, since "PEA (start & $ffff)" looks like // we're trying to use a (non-existent) indirect form of PEA. We can write things // in a simpler way. int adjustment, symbolValue; string symLabel = sym.Label; if (labelMap != null && labelMap.TryGetValue(symLabel, out string newLabel)) { symLabel = newLabel; } if (operandLen == 1) { // Use the byte-selection operator to get the right piece. string selOp; if (dfd.SymbolRef.ValuePart == WeakSymbolRef.Part.Bank) { symbolValue = (sym.Value >> 16) & 0xff; selOp = "^"; } else if (dfd.SymbolRef.ValuePart == WeakSymbolRef.Part.High) { symbolValue = (sym.Value >> 8) & 0xff; selOp = ">"; } else { symbolValue = sym.Value & 0xff; if (symbolValue == sym.Value) { selOp = string.Empty; } else { selOp = "<"; } } sb.Append(selOp); sb.Append(symLabel); operandValue &= 0xff; } else if (operandLen <= 4) { uint mask = 0xffffffff >> ((4 - operandLen) * 8); string shOp; if (dfd.SymbolRef.ValuePart == WeakSymbolRef.Part.Bank) { symbolValue = (sym.Value >> 16); shOp = " >> 16"; } else if (dfd.SymbolRef.ValuePart == WeakSymbolRef.Part.High) { symbolValue = (sym.Value >> 8); shOp = " >> 8"; } else { symbolValue = sym.Value; shOp = ""; } if (flags == FormatNumericOpFlags.IsPcRel) { // PC-relative operands are funny, because an 8- or 16-bit value is always // expanded to 24 bits. We output a 16-bit value that the assembler will // convert back to 8-bit or 16-bit. In any event, the bank byte is never // relevant to our computations. operandValue &= 0xffff; symbolValue &= 0xffff; } sb.Append(symLabel); sb.Append(shOp); if (symbolValue > mask) { // Post-shift value won't fit in an operand-size box. symbolValue = (int)(symbolValue & mask); sb.Append(" & "); sb.Append(formatter.FormatHexValue((int)mask, 2)); } operandValue = (int)(operandValue & mask); if (sb.Length != symLabel.Length) { sb.Append(' '); } } else { Debug.Assert(false, "bad numeric len"); sb.Append("?????"); symbolValue = 0; } adjustment = operandValue - symbolValue; sb.Append(formatter.FormatAdjustment(adjustment)); } /// /// Format the symbol and adjustment using Merlin expression syntax. /// private static void FormatNumericSymbolMerlin(Formatter formatter, Symbol sym, Dictionary labelMap, FormatDescriptor dfd, int operandValue, int operandLen, FormatNumericOpFlags flags, StringBuilder sb) { // Merlin expressions are compatible with the original 8-bit Merlin. They're // evaluated from left to right, with (almost) no regard for operator precedence. // // The part-selection operators differ from "simple" in two ways: // (1) They always happen last. If FOO=$10f0, "#>FOO+$18" == $11. One of the // few cases where left-to-right evaluation is overridden. // (2) They select words, not bytes. If FOO=$123456, "#>FOO" is $1234. This is // best thought of as a shift operator, rather than byte-selection. For // 8-bit code this doesn't matter. // // This behavior leads to simpler expressions for simple symbol adjustments. string symLabel = sym.Label; if (labelMap != null && labelMap.TryGetValue(symLabel, out string newLabel)) { symLabel = newLabel; } int adjustment; // If we add or subtract an adjustment, it will be done on the full value, which // is then shifted to the appropriate part. So we need to left-shift the operand // value to match. We fill in the low bytes with the contents of the symbol, so // that the adjustment doesn't include unnecessary values. (For example, let // FOO=$10f0, with operand "#>FOO" ($10). We shift the operand to get $1000, then // OR in the low byte to get $10f0, so that when we subtract we get adjustment==0.) int adjOperand, keepLen; if (dfd.SymbolRef.ValuePart == WeakSymbolRef.Part.Bank) { adjOperand = operandValue << 16 | (int)(sym.Value & 0xff00ffff); keepLen = 3; } else if (dfd.SymbolRef.ValuePart == WeakSymbolRef.Part.High) { adjOperand = (operandValue << 8) | (sym.Value & 0xff); keepLen = 2; } else { adjOperand = operandValue; keepLen = 1; } keepLen = Math.Max(keepLen, operandLen); adjustment = adjOperand - sym.Value; if (keepLen == 1) { adjustment %= 256; // Adjust for aesthetics. The assembler implicitly applies a modulo operation, // so we can use the value closest to zero. if (adjustment > 127) { adjustment = -(256 - adjustment) /*% 256*/; } else if (adjustment < -128) { adjustment = (256 + adjustment) /*% 256*/; } } else if (keepLen == 2) { adjustment %= 65536; if (adjustment > 32767) { adjustment = -(65536 - adjustment) /*% 65536*/; } else if (adjustment < -32768) { adjustment = (65536 + adjustment) /*% 65536*/; } } // Use the label from sym, not dfd's weak ref; might be different if label // comparisons are case-insensitive. switch (dfd.SymbolRef.ValuePart) { case WeakSymbolRef.Part.Unknown: case WeakSymbolRef.Part.Low: // For Merlin, "<" is effectively a no-op. We can put it in for // aesthetics when grabbing the low byte of a 16-bit value. if ((operandLen == 1) && sym.Value > 0xff) { sb.Append('<'); } sb.Append(symLabel); break; case WeakSymbolRef.Part.High: sb.Append('>'); sb.Append(symLabel); break; case WeakSymbolRef.Part.Bank: sb.Append('^'); sb.Append(symLabel); break; default: Debug.Assert(false, "bad part"); sb.Append("???"); break; } sb.Append(formatter.FormatAdjustment(adjustment)); } } }