diff --git a/SourceGenWPF/AsmGen/AsmCc65.cs b/SourceGenWPF/AsmGen/AsmCc65.cs new file mode 100644 index 0000000..3a1338a --- /dev/null +++ b/SourceGenWPF/AsmGen/AsmCc65.cs @@ -0,0 +1,869 @@ +/* + * 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.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Text; + +using Asm65; +using CommonUtil; + +namespace SourceGenWPF.AsmGen { + #region IGenerator + + /// + /// Generate source code compatible with the cc65 assembler (https://github.com/cc65/cc65). + /// + public class GenCc65 : IGenerator { + private const string ASM_FILE_SUFFIX = "_cc65.S"; // must start with underscore + private const string CFG_FILE_SUFFIX = "_cc65.cfg"; // ditto + private const int MAX_OPERAND_LEN = 64; + + // IGenerator + public DisasmProject Project { get; private set; } + + // IGenerator + public Formatter SourceFormatter { get; private set; } + + // IGenerator + public AppSettings Settings { get; private set; } + + // IGenerator + public AssemblerQuirks Quirks { get; private set; } + + // IGenerator + public LabelLocalizer Localizer { get { return mLocalizer; } } + + /// + /// Working directory, i.e. where we write our output file(s). + /// + private string mWorkDirectory; + + /// + /// If set, long labels get their own line. + /// + private bool mLongLabelNewLine; + + /// + /// Output column widths. + /// + private int[] mColumnWidths; + + /// + /// Base filename. Typically the project file name without the ".dis65" extension. + /// + private string mFileNameBase; + + /// + /// StringBuilder to use when composing a line. Held here to reduce allocations. + /// + private StringBuilder mLineBuilder = new StringBuilder(100); + + /// + /// Label localization helper. + /// + private LabelLocalizer mLocalizer; + + /// + /// Stream to send the output to. + /// + private StreamWriter mOutStream; + + /// + /// The first time we output a high-ASCII string, we generate a macro for it. + /// + private bool mHighAsciiMacroOutput; + + /// + /// Holds detected version of configured assembler. + /// + private CommonUtil.Version mAsmVersion = CommonUtil.Version.NO_VERSION; + + // We test against this in a few places. + private static CommonUtil.Version V2_17 = new CommonUtil.Version(2, 17); + + + // Pseudo-op string constants. + private static PseudoOp.PseudoOpNames sDataOpNames = new PseudoOp.PseudoOpNames() { + EquDirective = "=", + OrgDirective = ".org", + //RegWidthDirective // .a8, .a16, .i8, .i16 + DefineData1 = ".byte", + DefineData2 = ".word", + DefineData3 = ".faraddr", + DefineData4 = ".dword", + DefineBigData2 = ".dbyt", + //DefineBigData3 + //DefineBigData4 + Fill = ".res", + //Dense // no equivalent, use .byte with comma-separated args + StrGeneric = ".byte", + //StrReverse + StrNullTerm = ".asciiz", + //StrLen8 // macro with .strlen? + //StrLen16 + //StrDci + //StrDciReverse + }; + + + // IGenerator + public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps, + out Formatter.FormatConfig formatConfig) { + pseudoOps = new PseudoOp.PseudoOpNames() { + EquDirective = "=", + OrgDirective = ".org", + DefineData1 = ".byte", + DefineData2 = ".word", + DefineData3 = ".faraddr", + DefineData4 = ".dword", + DefineBigData2 = ".dbyt", + Fill = ".res", + StrGeneric = ".byte", + StrNullTerm = ".asciiz", + }; + + formatConfig = new Formatter.FormatConfig(); + SetFormatConfigValues(ref formatConfig); + } + + // IGenerator + public void Configure(DisasmProject project, string workDirectory, string fileNameBase, + AssemblerVersion asmVersion, AppSettings settings) { + Debug.Assert(project != null); + Debug.Assert(!string.IsNullOrEmpty(workDirectory)); + Debug.Assert(!string.IsNullOrEmpty(fileNameBase)); + + Project = project; + Quirks = new AssemblerQuirks(); + if (asmVersion != null) { + // Use the actual version. If it's > 2.17 we'll try to take advantage of + // bug fixes. + mAsmVersion = asmVersion.Version; + } else { + // No assembler installed. Use 2.17. + mAsmVersion = V2_17; + } + if (mAsmVersion <= V2_17) { + // cc65 v2.17: https://github.com/cc65/cc65/issues/717 + Quirks.BlockMoveArgsReversed = true; + // cc65 v2.17: https://github.com/cc65/cc65/issues/754 + Quirks.NoPcRelBankWrap = true; + } + Quirks.SinglePassAssembler = true; + + mWorkDirectory = workDirectory; + mFileNameBase = fileNameBase; + Settings = settings; + + mLongLabelNewLine = Settings.GetBool(AppSettings.SRCGEN_LONG_LABEL_NEW_LINE, false); + + AssemblerConfig config = AssemblerConfig.GetConfig(settings, + AssemblerInfo.Id.Cc65); + mColumnWidths = (int[])config.ColumnWidths.Clone(); + } + + /// + /// Configures the assembler-specific format items. + /// + private void SetFormatConfigValues(ref Formatter.FormatConfig config) { + config.mForceAbsOpcodeSuffix = string.Empty; + config.mForceLongOpcodeSuffix = string.Empty; + config.mForceDirectOperandPrefix = "z:"; // zero + config.mForceAbsOperandPrefix = "a:"; // absolute + config.mForceLongOperandPrefix = "f:"; // far + config.mEndOfLineCommentDelimiter = ";"; + config.mFullLineCommentDelimiterBase = ";"; + config.mBoxLineCommentDelimiter = ";"; + config.mAllowHighAsciiCharConst = false; + config.mExpressionMode = Formatter.FormatConfig.ExpressionMode.Cc65; + } + + // IGenerator + public List GenerateSource(BackgroundWorker worker) { + List pathNames = new List(1); + + string pathName = Path.Combine(mWorkDirectory, mFileNameBase + ASM_FILE_SUFFIX); + pathNames.Add(pathName); + string cfgName = Path.Combine(mWorkDirectory, mFileNameBase + CFG_FILE_SUFFIX); + pathNames.Add(cfgName); + + Formatter.FormatConfig config = new Formatter.FormatConfig(); + GenCommon.ConfigureFormatterFromSettings(Settings, ref config); + SetFormatConfigValues(ref config); + SourceFormatter = new Formatter(config); + + string msg = string.Format(Res.Strings.PROGRESS_GENERATING_FMT, pathName); + worker.ReportProgress(0, msg); + + mLocalizer = new LabelLocalizer(Project); + if (!Settings.GetBool(AppSettings.SRCGEN_DISABLE_LABEL_LOCALIZATION, false)) { + mLocalizer.LocalPrefix = "@"; + mLocalizer.Analyze(); + } + + // Use UTF-8 encoding, without a byte-order mark. + using (StreamWriter sw = new StreamWriter(cfgName, false, new UTF8Encoding(false))) { + GenerateLinkerScript(sw); + } + using (StreamWriter sw = new StreamWriter(pathName, false, new UTF8Encoding(false))) { + mOutStream = sw; + + if (Settings.GetBool(AppSettings.SRCGEN_ADD_IDENT_COMMENT, false)) { + //if (mAsmVersion.IsValid && mAsmVersion <= V2_17) { + // OutputLine(SourceFormatter.FullLineCommentDelimiter + + // string.Format(Properties.Resources.GENERATED_FOR_VERSION, + // "cc65", mAsmVersion.ToString())); + //} else { + // OutputLine(SourceFormatter.FullLineCommentDelimiter + + // string.Format(Properties.Resources.GENERATED_FOR_LATEST, "cc65")); + //} + + // Currently generating code for v2.17. + OutputLine(SourceFormatter.FullLineCommentDelimiter + + string.Format(Res.Strings.GENERATED_FOR_VERSION_FMT, + "cc65", V2_17, + AsmCc65.OPTIONS + " -C " + Path.GetFileName(cfgName))); + } + + GenCommon.Generate(this, sw, worker); + } + mOutStream = null; + + return pathNames; + } + + private void GenerateLinkerScript(StreamWriter sw) { + sw.WriteLine("# 6502bench SourceGen generated linker script for " + mFileNameBase); + + sw.WriteLine("MEMORY {"); + sw.WriteLine(" MAIN: file=%O, start=%S, size=65536;"); + for (int i = 0; i < Project.AddrMap.Count; i++) { + AddressMap.AddressMapEntry ame = Project.AddrMap[i]; + sw.WriteLine(string.Format("# MEM{0:D3}: file=%O, start=${1:x4}, size={2};", + i, ame.Addr, ame.Length)); + } + sw.WriteLine("}"); + + sw.WriteLine("SEGMENTS {"); + sw.WriteLine(" CODE: load=MAIN, type=rw;"); + for (int i = 0; i < Project.AddrMap.Count; i++) { + sw.WriteLine(string.Format("# SEG{0:D3}: load=MEM{0:D3}, type=rw;", i)); + } + sw.WriteLine("}"); + + sw.WriteLine("FEATURES {}"); + sw.WriteLine("SYMBOLS {}"); + } + + // IGenerator + public void OutputAsmConfig() { + CpuDef cpuDef = Project.CpuDef; + string cpuStr; + if (cpuDef.Type == CpuDef.CpuType.Cpu65816) { + cpuStr = "65816"; + } else if (cpuDef.Type == CpuDef.CpuType.Cpu65C02) { + cpuStr = "65C02"; + } else if (cpuDef.Type == CpuDef.CpuType.Cpu6502 && cpuDef.HasUndocumented) { + cpuStr = "6502X"; + } else { + cpuStr = "6502"; + } + + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(".setcpu"), + '\"' + cpuStr + '\"', string.Empty); + } + + /// + /// Map the undocumented opcodes to the cc65 mnemonics. There's almost no difference + /// vs. the Unintended Opcodes mnemonics. + /// + /// We don't include the double- and triple-byte NOPs here, as cc65 doesn't + /// appear to have a definition for them (as of 2.17). We also omit the alias + /// for SBC. These will all be output as hex. + /// + private static Dictionary sUndocMap = new Dictionary() { + { OpName.ALR, "alr" }, // imm 0x4b + { OpName.ANC, "anc" }, // imm 0x0b (and others) + { OpName.ANE, "ane" }, // imm 0x8b + { OpName.ARR, "arr" }, // imm 0x6b + { OpName.DCP, "dcp" }, // abs 0xcf + { OpName.ISC, "isc" }, // abs 0xef + { OpName.JAM, "jam" }, // abs 0x02 (and others) + { OpName.LAS, "las" }, // abs,y 0xbb + { OpName.LAX, "lax" }, // imm 0xab; abs 0xaf + { OpName.RLA, "rla" }, // abs 0x2f + { OpName.RRA, "rra" }, // abs 0x6f + { OpName.SAX, "sax" }, // abs 0x8f + { OpName.SBX, "axs" }, //* imm 0xcb + { OpName.SHA, "sha" }, // abs,y 0x9f + { OpName.SHX, "shx" }, // abs,y 0x9e + { OpName.SHY, "shy" }, // abs,x 0x9c + { OpName.SLO, "slo" }, // abs 0x0f + { OpName.SRE, "sre" }, // abs 0x4f + { OpName.TAS, "tas" }, // abs,y 0x9b + }; + + // IGenerator + public string ModifyOpcode(int offset, OpDef op) { + if ((op == OpDef.OpWDM_WDM || op == OpDef.OpBRK_StackInt) && mAsmVersion <= V2_17) { + // cc65 v2.17 doesn't support WDM, and assembles BRK to opcode $05. + // https://github.com/cc65/cc65/issues/715 + // https://github.com/cc65/cc65/issues/716 + return null; + } else if (op.IsUndocumented) { + if (sUndocMap.TryGetValue(op.Mnemonic, out string newValue)) { + if ((op.Mnemonic == OpName.ANC && op.Opcode != 0x0b) || + (op.Mnemonic == OpName.JAM && op.Opcode != 0x02)) { + // There are multiple opcodes for the same thing. cc65 outputs + // one specific thing, so we need to match that, and just do a hex + // dump for the others. + return null; + } + return newValue; + } + // Unmapped values include DOP, TOP, and the alternate SBC. Output hex. + return null; + } else { + return string.Empty; + } + } + + // IGenerator + public void GenerateShortSequence(int offset, int length, out string opcode, + out string operand) { + Debug.Assert(length >= 1 && length <= 4); + + // Use a comma-separated list of individual hex bytes. + opcode = sDataOpNames.DefineData1; + + StringBuilder sb = new StringBuilder(length * 4); + for (int i = 0; i < length; i++) { + if (i != 0) { + sb.Append(','); + } + sb.Append(SourceFormatter.FormatHexValue(Project.FileData[offset + i], 2)); + } + operand = sb.ToString(); + } + + // IGenerator + public void OutputDataOp(int offset) { + Formatter formatter = SourceFormatter; + byte[] data = Project.FileData; + Anattrib attr = Project.GetAnattrib(offset); + + string labelStr = string.Empty; + if (attr.Symbol != null) { + labelStr = mLocalizer.ConvLabel(attr.Symbol.Label); + } + + string commentStr = SourceFormatter.FormatEolComment(Project.Comments[offset]); + string opcodeStr, operandStr; + + FormatDescriptor dfd = attr.DataDescriptor; + Debug.Assert(dfd != null); + int length = dfd.Length; + Debug.Assert(length > 0); + + bool multiLine = false; + switch (dfd.FormatType) { + case FormatDescriptor.Type.Default: + if (length != 1) { + Debug.Assert(false); + length = 1; + } + opcodeStr = sDataOpNames.DefineData1; + int operand = RawData.GetWord(data, offset, length, false); + operandStr = formatter.FormatHexValue(operand, length * 2); + break; + case FormatDescriptor.Type.NumericLE: + opcodeStr = sDataOpNames.GetDefineData(length); + operand = RawData.GetWord(data, offset, length, false); + operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable, + mLocalizer.LabelMap, dfd, operand, length, + PseudoOp.FormatNumericOpFlags.None); + break; + case FormatDescriptor.Type.NumericBE: + opcodeStr = sDataOpNames.GetDefineBigData(length); + if (opcodeStr == null) { + // Nothing defined, output as comma-separated single-byte values. + GenerateShortSequence(offset, length, out opcodeStr, out operandStr); + } else { + operand = RawData.GetWord(data, offset, length, true); + operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable, + mLocalizer.LabelMap, dfd, operand, length, + PseudoOp.FormatNumericOpFlags.None); + } + break; + case FormatDescriptor.Type.Fill: + opcodeStr = sDataOpNames.Fill; + operandStr = length + "," + formatter.FormatHexValue(data[offset], 2); + break; + case FormatDescriptor.Type.Dense: + multiLine = true; + opcodeStr = operandStr = null; + OutputDenseHex(offset, length, labelStr, commentStr); + break; + case FormatDescriptor.Type.String: + multiLine = true; + opcodeStr = operandStr = null; + OutputString(offset, labelStr, commentStr); + break; + default: + opcodeStr = "???"; + operandStr = "***"; + break; + } + + if (!multiLine) { + opcodeStr = formatter.FormatPseudoOp(opcodeStr); + OutputLine(labelStr, opcodeStr, operandStr, commentStr); + } + } + + private void OutputDenseHex(int offset, int length, string labelStr, string commentStr) { + Formatter formatter = SourceFormatter; + byte[] data = Project.FileData; + StringBuilder sb = new StringBuilder(MAX_OPERAND_LEN); + + string opcodeStr = formatter.FormatPseudoOp(sDataOpNames.DefineData1); + + int maxPerLine = MAX_OPERAND_LEN / 4; + int numChunks = (length + maxPerLine - 1) / maxPerLine; + for (int chunk = 0; chunk < numChunks; chunk++) { + int chunkStart = chunk * maxPerLine; + int chunkEnd = Math.Min((chunk + 1) * maxPerLine, length); + for (int i = chunkStart; i < chunkEnd; i++) { + if (i != chunkStart) { + sb.Append(','); + } + sb.Append(formatter.FormatHexValue(data[offset + i], 2)); + } + + OutputLine(labelStr, opcodeStr, sb.ToString(), commentStr); + labelStr = commentStr = string.Empty; + sb.Clear(); + } + } + + /// + /// Outputs formatted data in an unformatted way, because the code generator couldn't + /// figure out how to do something better. + /// + private void OutputNoJoy(int offset, int length, string labelStr, string commentStr) { + byte[] data = Project.FileData; + Debug.Assert(length > 0); + Debug.Assert(offset >= 0 && offset < data.Length); + + bool singleValue = true; + byte val = data[offset]; + for (int i = 1; i < length; i++) { + if (data[offset + i] != val) { + singleValue = false; + break; + } + } + + if (singleValue) { + string opcodeStr = SourceFormatter.FormatPseudoOp(sDataOpNames.Fill); + string operandStr = length + "," + SourceFormatter.FormatHexValue(val, 2); + OutputLine(labelStr, opcodeStr, operandStr, commentStr); + } else { + OutputDenseHex(offset, length, labelStr, commentStr); + } + } + + // IGenerator + public void OutputEquDirective(string name, string valueStr, string comment) { + OutputLine(name, SourceFormatter.FormatPseudoOp(sDataOpNames.EquDirective), + valueStr, SourceFormatter.FormatEolComment(comment)); + } + + // IGenerator + public void OutputOrgDirective(int offset, int address) { + // Linear search for offset. List should be small, so this should be quick. + int index = 0; + foreach (AddressMap.AddressMapEntry ame in Project.AddrMap) { + if (ame.Offset == offset) { + break; + } + index++; + } + + mLineBuilder.Clear(); + TextUtil.AppendPaddedString(mLineBuilder, ";", mColumnWidths[0]); + TextUtil.AppendPaddedString(mLineBuilder, SourceFormatter.FormatPseudoOp(" .segment"), + mColumnWidths[1]); + mLineBuilder.AppendFormat("\"SEG{0:D3}\"", index); + OutputLine(mLineBuilder.ToString()); + + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(sDataOpNames.OrgDirective), + SourceFormatter.FormatHexValue(address, 4), string.Empty); + } + + // IGenerator + public void OutputRegWidthDirective(int offset, int prevM, int prevX, int newM, int newX) { + if (prevM != newM) { + string mop = (newM == 0) ? ".a16" : ".a8"; + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(mop), + string.Empty, string.Empty); + } + if (prevX != newX) { + string xop = (newX == 0) ? ".i16" : ".i8"; + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(xop), + string.Empty, string.Empty); + } + } + + // IGenerator + public void OutputLine(string fullLine) { + mOutStream.WriteLine(fullLine); + } + + // IGenerator + public void OutputLine(string label, string opcode, string operand, string comment) { + // If a label is provided, and it doesn't start with a '.' (indicating that it's + // a directive), and this isn't an EQU directive, add a ':'. Might be easier to + // just ".feature labels_without_colons", but I'm trying to do things the way + // that cc65 users will expect. + if (!string.IsNullOrEmpty(label) && label[0] != '.' && + !string.Equals(opcode, sDataOpNames.EquDirective, + StringComparison.InvariantCultureIgnoreCase)) { + label += ':'; + + if (mLongLabelNewLine && label.Length >= mColumnWidths[0]) { + mOutStream.WriteLine(label); + label = string.Empty; + } + } + + mLineBuilder.Clear(); + TextUtil.AppendPaddedString(mLineBuilder, label, mColumnWidths[0]); + TextUtil.AppendPaddedString(mLineBuilder, opcode, mColumnWidths[0] + mColumnWidths[1]); + TextUtil.AppendPaddedString(mLineBuilder, operand, + mColumnWidths[0] + mColumnWidths[1] + mColumnWidths[2]); + if (string.IsNullOrEmpty(comment)) { + // Trim trailing spaces off of opcode or operand. If they want trailing + // spaces at the end of a comment, that's fine. + CommonUtil.TextUtil.TrimEnd(mLineBuilder); + } else { + mLineBuilder.Append(comment); + } + + mOutStream.WriteLine(mLineBuilder.ToString()); + } + + private void OutputString(int offset, string labelStr, string commentStr) { + // Normal ASCII strings are straightforward: they're just part of a .byte + // directive, and can mix with anything else in the .byte. + // + // For CString we can use .asciiz, but only if the string fits on one line + // and doesn't include delimiters. For L8String and L16String we can + // define simple macros, but their use has a similar restriction. High-ASCII + // strings also require a macro. + // + // We might be able to define a macro for DCI and Reverse as well. + // + // The limitation on strings with delimiters arises because (1) I don't see a + // way to escape them within a string, and (2) the simple macro workarounds + // only take a single argument, not a comma-separated list of stuff. + // + // Some ideas here: + // https://groups.google.com/forum/#!topic/comp.sys.apple2.programmer/5Wkw8mUPcU0 + + Formatter formatter = SourceFormatter; + byte[] data = Project.FileData; + Anattrib attr = Project.GetAnattrib(offset); + FormatDescriptor dfd = attr.DataDescriptor; + Debug.Assert(dfd != null); + Debug.Assert(dfd.FormatType == FormatDescriptor.Type.String); + Debug.Assert(dfd.Length > 0); + + bool highAscii = false; + int leadingBytes = 0; + int trailingBytes = 0; + bool showLeading = false; + bool showTrailing = false; + + switch (dfd.FormatSubType) { + case FormatDescriptor.SubType.None: + highAscii = (data[offset] & 0x80) != 0; + break; + case FormatDescriptor.SubType.Dci: + highAscii = (data[offset] & 0x80) != 0; + trailingBytes = 1; + showTrailing = true; + break; + case FormatDescriptor.SubType.Reverse: + highAscii = (data[offset] & 0x80) != 0; + break; + case FormatDescriptor.SubType.DciReverse: + highAscii = (data[offset + dfd.Length - 1] & 0x80) != 0; + leadingBytes = 1; + showLeading = true; + break; + case FormatDescriptor.SubType.CString: + highAscii = (data[offset] & 0x80) != 0; + trailingBytes = 1; + showTrailing = true; + break; + case FormatDescriptor.SubType.L8String: + if (dfd.Length > 1) { + highAscii = (data[offset + 1] & 0x80) != 0; + } + leadingBytes = 1; + showLeading = true; + break; + case FormatDescriptor.SubType.L16String: + if (dfd.Length > 2) { + highAscii = (data[offset + 2] & 0x80) != 0; + } + leadingBytes = 2; + showLeading = true; + break; + default: + Debug.Assert(false); + return; + } + + char delim = '"'; + StringGather gath = null; + + // Run the string through so we can see if it'll fit on one line. As a minor + // optimization, we skip this step for "generic" strings, which are probably + // the most common thing. + if (dfd.FormatSubType != FormatDescriptor.SubType.None || highAscii) { + gath = new StringGather(this, labelStr, "???", commentStr, delim, + delim, StringGather.ByteStyle.CommaSep, MAX_OPERAND_LEN, true); + FeedGath(gath, data, offset, dfd.Length, leadingBytes, showLeading, + trailingBytes, showTrailing); + Debug.Assert(gath.NumLinesOutput > 0); + } + + string opcodeStr = formatter.FormatPseudoOp(sDataOpNames.StrGeneric); + + switch (dfd.FormatSubType) { + case FormatDescriptor.SubType.None: + // Special case for simple short high-ASCII strings. These have no + // leading or trailing bytes. We can improve this a bit by handling + // arbitrarily long strings by simply breaking them across lines. + Debug.Assert(leadingBytes == 0); + Debug.Assert(trailingBytes == 0); + if (highAscii && gath.NumLinesOutput == 1 && !gath.HasDelimiter) { + if (!mHighAsciiMacroOutput) { + mHighAsciiMacroOutput = true; + // Output a macro for high-ASCII strings. + OutputLine(".macro", "HiAscii", "Arg", string.Empty); + OutputLine(string.Empty, ".repeat", ".strlen(Arg), I", string.Empty); + OutputLine(string.Empty, ".byte", ".strat(Arg, I) | $80", string.Empty); + OutputLine(string.Empty, ".endrep", string.Empty, string.Empty); + OutputLine(".endmacro", string.Empty, string.Empty, string.Empty); + } + opcodeStr = formatter.FormatPseudoOp("HiAscii"); + highAscii = false; + } + break; + case FormatDescriptor.SubType.Dci: + case FormatDescriptor.SubType.Reverse: + case FormatDescriptor.SubType.DciReverse: + // Full configured above. + break; + case FormatDescriptor.SubType.CString: + if (gath.NumLinesOutput == 1 && !gath.HasDelimiter) { + opcodeStr = sDataOpNames.StrNullTerm; + showTrailing = false; + } + break; + case FormatDescriptor.SubType.L8String: + case FormatDescriptor.SubType.L16String: + // Implement macros? + break; + default: + Debug.Assert(false); + return; + } + + if (highAscii) { + OutputNoJoy(offset, dfd.Length, labelStr, commentStr); + return; + } + + // Create a new StringGather, with the final opcode choice. + gath = new StringGather(this, labelStr, opcodeStr, commentStr, delim, + delim, StringGather.ByteStyle.CommaSep, MAX_OPERAND_LEN, false); + FeedGath(gath, data, offset, dfd.Length, leadingBytes, showLeading, + trailingBytes, showTrailing); + } + + /// + /// Feeds the bytes into the StringGather. + /// + private void FeedGath(StringGather gath, byte[] data, int offset, int length, + int leadingBytes, bool showLeading, int trailingBytes, bool showTrailing) { + int startOffset = offset; + int strEndOffset = offset + length - trailingBytes; + + if (showLeading) { + while (leadingBytes-- > 0) { + gath.WriteByte(data[offset++]); + } + } else { + offset += leadingBytes; + } + for (; offset < strEndOffset; offset++) { + gath.WriteChar((char)(data[offset] & 0x7f)); + } + while (showTrailing && trailingBytes-- > 0) { + gath.WriteByte(data[offset++]); + } + gath.Finish(); + } + } + + #endregion IGenerator + + + #region IAssembler + + /// + /// Cross-assembler execution interface. + /// + public class AsmCc65 : IAssembler { + // Fixed options. "--target none" is needed to neutralize the character encoding, + // which seems to default to PETSCII. + public const string OPTIONS = "--target none"; + + // Paths from generator. + private List mPathNames; + + // Directory to make current before executing assembler. + private string mWorkDirectory; + + + // IAssembler + public void GetExeIdentifiers(out string humanName, out string exeName) { + humanName = "cc65 CL"; + exeName = "cl65"; + } + + // IAssembler + public AssemblerConfig GetDefaultConfig() { + return new AssemblerConfig(string.Empty, new int[] { 9, 8, 11, 72 }); + } + + // IAssembler + public AssemblerVersion QueryVersion() { + AssemblerConfig config = + AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Cc65); + if (config == null || string.IsNullOrEmpty(config.ExecutablePath)) { + return null; + } + + ShellCommand cmd = new ShellCommand(config.ExecutablePath, "--version", + Directory.GetCurrentDirectory(), null); + cmd.Execute(); + if (string.IsNullOrEmpty(cmd.Stdout)) { + return null; + } + + // Windows - Stderr: "cl65.exe V2.17\r\n" + // Linux - Stderr: "cl65 V2.17 - Git N/A\n" + // Other platforms may not have the ".exe". Find first occurrence of " V". + + const string PREFIX = " V"; + string str = cmd.Stderr; + int start = str.IndexOf(PREFIX); + int end = (start < 0) ? -1 : str.IndexOfAny(new char[] { ' ', '\r', '\n' }, start + 1); + + if (start < 0 || end < 0 || start + PREFIX.Length >= end) { + Debug.WriteLine("Couldn't find version in " + str); + return null; + } + start += PREFIX.Length; + string versionStr = str.Substring(start, end - start); + CommonUtil.Version version = CommonUtil.Version.Parse(versionStr); + if (!version.IsValid) { + return null; + } + return new AssemblerVersion(versionStr, version); + } + + // IAssembler + public void Configure(List pathNames, string workDirectory) { + // Clone pathNames, in case the caller decides to modify the original. + mPathNames = new List(pathNames.Count); + foreach (string str in pathNames) { + mPathNames.Add(str); + } + + mWorkDirectory = workDirectory; + } + + // IAssembler + public AssemblerResults RunAssembler(BackgroundWorker worker) { + Debug.Assert(mPathNames.Count == 2); + string pathName = StripWorkDirectory(mPathNames[0]); + string cfgName = StripWorkDirectory(mPathNames[1]); + + AssemblerConfig config = + AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Cc65); + if (string.IsNullOrEmpty(config.ExecutablePath)) { + Debug.WriteLine("Assembler not configured"); + return null; + } + + string cfgOpt = " -C \"" + cfgName + "\""; + + worker.ReportProgress(0, Res.Strings.PROGRESS_ASSEMBLING); + + // Wrap pathname in quotes in case it has spaces. + // (Do we need to shell-escape quotes in the pathName?) + ShellCommand cmd = new ShellCommand(config.ExecutablePath, + OPTIONS + cfgOpt + " \"" + pathName + "\"", mWorkDirectory, null); + cmd.Execute(); + + // Can't really do anything with a "cancel" request. + + // Output filename is the input filename without the ".S". Since the filename + // was generated by us we can be confident in the format. + string outputFile = mPathNames[0].Substring(0, mPathNames[0].Length - 2); + + return new AssemblerResults(cmd.FullCommandLine, cmd.ExitCode, cmd.Stdout, + cmd.Stderr, outputFile); + } + + /// + /// Reduce input file to a partial path if possible. This is just to make + /// what we display to the user a little easier to read. + /// + /// Full pathname of file. + /// Pathname with working directory prefix stripped off. + private string StripWorkDirectory(string pathName) { + if (pathName.StartsWith(mWorkDirectory)) { + return pathName.Remove(0, mWorkDirectory.Length + 1); + } else { + // Unexpected, but shouldn't be a problem. + Debug.WriteLine("NOTE: source file is not in work directory"); + return pathName; + } + } + } + + #endregion IAssembler +} diff --git a/SourceGenWPF/AsmGen/AsmMerlin32.cs b/SourceGenWPF/AsmGen/AsmMerlin32.cs new file mode 100644 index 0000000..8d18c6b --- /dev/null +++ b/SourceGenWPF/AsmGen/AsmMerlin32.cs @@ -0,0 +1,790 @@ +/* + * 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.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Text; + +using Asm65; +using CommonUtil; + +namespace SourceGenWPF.AsmGen { + #region IGenerator + + /// + /// Generate source code compatible with Brutal Deluxe's Merlin 32 assembler + /// (https://www.brutaldeluxe.fr/products/crossdevtools/merlin/). + /// + public class GenMerlin32 : IGenerator { + private const string ASM_FILE_SUFFIX = "_Merlin32.S"; // must start with underscore + private const int MAX_OPERAND_LEN = 64; + + // IGenerator + public DisasmProject Project { get; private set; } + + // IGenerator + public Formatter SourceFormatter { get; private set; } + + // IGenerator + public AppSettings Settings { get; private set; } + + // IGenerator + public AssemblerQuirks Quirks { get; private set; } + + // IGenerator + public LabelLocalizer Localizer { get { return mLocalizer; } } + + /// + /// Working directory, i.e. where we write our output file(s). + /// + private string mWorkDirectory; + + /// + /// If set, long labels get their own line. + /// + private bool mLongLabelNewLine; + + /// + /// Output column widths. + /// + private int[] mColumnWidths; + + /// + /// Base filename. Typically the project file name without the ".dis65" extension. + /// + private string mFileNameBase; + + /// + /// StringBuilder to use when composing a line. Held here to reduce allocations. + /// + private StringBuilder mLineBuilder = new StringBuilder(100); + + /// + /// Label localization helper. + /// + private LabelLocalizer mLocalizer; + + /// + /// Stream to send the output to. + /// + private StreamWriter mOutStream; + + /// + /// Holds detected version of configured assembler. + /// + private CommonUtil.Version mAsmVersion = CommonUtil.Version.NO_VERSION; + + + // Semi-convenient way to hold all the interesting string constants in one place. + // Note the actual usage of the pseudo-op may not match what the main app does, + // e.g. RegWidthDirective behaves differently from "mx". I'm just trying to avoid + // having string constants scattered all over. + private static PseudoOp.PseudoOpNames sDataOpNames = new PseudoOp.PseudoOpNames() { + EquDirective = "equ", + OrgDirective = "org", + RegWidthDirective = "mx", + DefineData1 = "dfb", + DefineData2 = "dw", + DefineData3 = "adr", + DefineData4 = "adrl", + DefineBigData2 = "ddb", + //DefineBigData3 + //DefineBigData4 + Fill = "ds", + Dense = "hex", + StrGeneric = "asc", + StrGenericHi = "asc", + StrReverse = "rev", + StrReverseHi = "rev", + //StrNullTerm + StrLen8 = "str", + StrLen8Hi = "str", + StrLen16 = "strl", + StrLen16Hi = "strl", + StrDci = "dci", + StrDciHi = "dci", + //StrDciReverse + }; + + + // IGenerator + public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps, + out Formatter.FormatConfig formatConfig) { + // This is not intended to match up with the Merlin generator, which uses + // the same pseudo-op for low/high ASCII but different string delimiters. We + // don't change the delimiters for the display list, so instead we tweak the + // opcode slightly. + char hiAscii = '\u2191'; + pseudoOps = new PseudoOp.PseudoOpNames() { + EquDirective = "equ", + OrgDirective = "org", + DefineData1 = "dfb", + DefineData2 = "dw", + DefineData3 = "adr", + DefineData4 = "adrl", + DefineBigData2 = "ddb", + Fill = "ds", + Dense = "hex", + StrGeneric = "asc", + StrGenericHi = "asc" + hiAscii, + StrReverse = "rev", + StrReverseHi = "rev" + hiAscii, + StrLen8 = "str", + StrLen8Hi = "str" + hiAscii, + StrLen16 = "strl", + StrLen16Hi = "strl" + hiAscii, + StrDci = "dci", + StrDciHi = "dci" + hiAscii, + }; + + formatConfig = new Formatter.FormatConfig(); + SetFormatConfigValues(ref formatConfig); + } + + // IGenerator + public void Configure(DisasmProject project, string workDirectory, string fileNameBase, + AssemblerVersion asmVersion, AppSettings settings) { + Debug.Assert(project != null); + Debug.Assert(!string.IsNullOrEmpty(workDirectory)); + Debug.Assert(!string.IsNullOrEmpty(fileNameBase)); + + Project = project; + Quirks = new AssemblerQuirks(); + Quirks.TracksSepRepNotEmu = true; + Quirks.NoPcRelBankWrap = true; + + mWorkDirectory = workDirectory; + mFileNameBase = fileNameBase; + Settings = settings; + + mLongLabelNewLine = Settings.GetBool(AppSettings.SRCGEN_LONG_LABEL_NEW_LINE, false); + + AssemblerConfig config = AssemblerConfig.GetConfig(settings, + AssemblerInfo.Id.Merlin32); + mColumnWidths = (int[])config.ColumnWidths.Clone(); + } + + /// + /// Configures the assembler-specific format items. + /// + private void SetFormatConfigValues(ref Formatter.FormatConfig config) { + config.mForceAbsOpcodeSuffix = ":"; + config.mForceLongOpcodeSuffix = "l"; + config.mForceDirectOperandPrefix = string.Empty; + config.mForceAbsOperandPrefix = string.Empty; + config.mForceLongOperandPrefix = string.Empty; + config.mEndOfLineCommentDelimiter = ";"; + config.mFullLineCommentDelimiterBase = ";"; + config.mBoxLineCommentDelimiter = string.Empty; + config.mAllowHighAsciiCharConst = true; + config.mExpressionMode = Formatter.FormatConfig.ExpressionMode.Merlin; + } + + // IGenerator; executes on background thread + public List GenerateSource(BackgroundWorker worker) { + List pathNames = new List(1); + + string fileName = mFileNameBase + ASM_FILE_SUFFIX; + string pathName = Path.Combine(mWorkDirectory, fileName); + pathNames.Add(pathName); + + Formatter.FormatConfig config = new Formatter.FormatConfig(); + GenCommon.ConfigureFormatterFromSettings(Settings, ref config); + SetFormatConfigValues(ref config); + SourceFormatter = new Formatter(config); + + string msg = string.Format(Res.Strings.PROGRESS_GENERATING_FMT, pathName); + worker.ReportProgress(0, msg); + + mLocalizer = new LabelLocalizer(Project); + if (!Settings.GetBool(AppSettings.SRCGEN_DISABLE_LABEL_LOCALIZATION, false)) { + mLocalizer.LocalPrefix = ":"; + mLocalizer.Analyze(); + } + + // Use UTF-8 encoding, without a byte-order mark. + using (StreamWriter sw = new StreamWriter(pathName, false, new UTF8Encoding(false))) { + mOutStream = sw; + + if (Settings.GetBool(AppSettings.SRCGEN_ADD_IDENT_COMMENT, false)) { + // No version-specific stuff yet. We're generating code for v1.0. + OutputLine(SourceFormatter.FullLineCommentDelimiter + + string.Format(Res.Strings.GENERATED_FOR_VERSION_FMT, + "Merlin 32", new CommonUtil.Version(1, 0), string.Empty)); + } + + GenCommon.Generate(this, sw, worker); + } + mOutStream = null; + + return pathNames; + } + + // IGenerator + public void OutputDataOp(int offset) { + Formatter formatter = SourceFormatter; + byte[] data = Project.FileData; + Anattrib attr = Project.GetAnattrib(offset); + + string labelStr = string.Empty; + if (attr.Symbol != null) { + labelStr = mLocalizer.ConvLabel(attr.Symbol.Label); + } + + string commentStr = SourceFormatter.FormatEolComment(Project.Comments[offset]); + string opcodeStr, operandStr; + + FormatDescriptor dfd = attr.DataDescriptor; + Debug.Assert(dfd != null); + int length = dfd.Length; + Debug.Assert(length > 0); + + bool multiLine = false; + switch (dfd.FormatType) { + case FormatDescriptor.Type.Default: + if (length != 1) { + Debug.Assert(false); + length = 1; + } + opcodeStr = sDataOpNames.DefineData1; + int operand = RawData.GetWord(data, offset, length, false); + operandStr = formatter.FormatHexValue(operand, length * 2); + break; + case FormatDescriptor.Type.NumericLE: + opcodeStr = sDataOpNames.GetDefineData(length); + operand = RawData.GetWord(data, offset, length, false); + operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable, + mLocalizer.LabelMap, dfd, operand, length, + PseudoOp.FormatNumericOpFlags.None); + break; + case FormatDescriptor.Type.NumericBE: + opcodeStr = sDataOpNames.GetDefineBigData(length); + if (opcodeStr == null) { + // Nothing defined, output as comma-separated single-byte values. + GenerateShortSequence(offset, length, out opcodeStr, out operandStr); + } else { + operand = RawData.GetWord(data, offset, length, true); + operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable, + mLocalizer.LabelMap, dfd, operand, length, + PseudoOp.FormatNumericOpFlags.None); + } + break; + case FormatDescriptor.Type.Fill: + opcodeStr = sDataOpNames.Fill; + operandStr = length + "," + formatter.FormatHexValue(data[offset], 2); + break; + case FormatDescriptor.Type.Dense: + multiLine = true; + opcodeStr = operandStr = null; + OutputDenseHex(offset, length, labelStr, commentStr); + break; + case FormatDescriptor.Type.String: + multiLine = true; + opcodeStr = operandStr = null; + OutputString(offset, labelStr, commentStr); + break; + default: + opcodeStr = "???"; + operandStr = "***"; + break; + } + + if (!multiLine) { + opcodeStr = formatter.FormatPseudoOp(opcodeStr); + OutputLine(labelStr, opcodeStr, operandStr, commentStr); + } + } + + private void OutputDenseHex(int offset, int length, string labelStr, string commentStr) { + Formatter formatter = SourceFormatter; + byte[] data = Project.FileData; + int maxPerLine = MAX_OPERAND_LEN / 2; + + string opcodeStr = formatter.FormatPseudoOp(sDataOpNames.Dense); + for (int i = 0; i < length; i += maxPerLine) { + int subLen = length - i; + if (subLen > maxPerLine) { + subLen = maxPerLine; + } + string operandStr = formatter.FormatDenseHex(data, offset + i, subLen); + + OutputLine(labelStr, opcodeStr, operandStr, commentStr); + labelStr = commentStr = string.Empty; + } + } + + // IGenerator + public string ModifyOpcode(int offset, OpDef op) { + if (op.IsUndocumented) { + return null; + } + + // The assembler works correctly if the symbol is defined as a two-digit hex + // value (e.g. "foo equ $80") but fails if it's four (e.g. "foo equ $0080"). We + // output symbols with minimal digits, but this doesn't help if the code itself + // lives on zero page. If the operand is a reference to a zero-page user label, + // we need to output the instruction as hex. + // More info: https://github.com/apple2accumulator/merlin32/issues/8 + if (op == OpDef.OpPEI_StackDPInd || + op == OpDef.OpSTY_DPIndexX || + op == OpDef.OpSTX_DPIndexY || + op.AddrMode == OpDef.AddressMode.DPIndLong || + op.AddrMode == OpDef.AddressMode.DPInd || + op.AddrMode == OpDef.AddressMode.DPIndexXInd) { + FormatDescriptor dfd = Project.GetAnattrib(offset).DataDescriptor; + if (dfd != null && dfd.HasSymbol) { + // It has a symbol. See if the symbol target is a label (auto or user). + if (Project.SymbolTable.TryGetValue(dfd.SymbolRef.Label, out Symbol sym)) { + if (sym.IsInternalLabel) { + return null; + } + } + } + } + + return string.Empty; + } + + // IGenerator + public void GenerateShortSequence(int offset, int length, out string opcode, + out string operand) { + Debug.Assert(length >= 1 && length <= 4); + + // Use a comma-separated list of individual hex bytes. + opcode = sDataOpNames.DefineData1; + + StringBuilder sb = new StringBuilder(length * 4); + for (int i = 0; i < length; i++) { + if (i != 0) { + sb.Append(','); + } + sb.Append(SourceFormatter.FormatHexValue(Project.FileData[offset + i], 2)); + } + operand = sb.ToString(); + } + + // IGenerator + public void OutputAsmConfig() { + // nothing to do + } + + // IGenerator + public void OutputEquDirective(string name, string valueStr, string comment) { + OutputLine(name, SourceFormatter.FormatPseudoOp(sDataOpNames.EquDirective), + valueStr, SourceFormatter.FormatEolComment(comment)); + } + + // IGenerator + public void OutputOrgDirective(int offset, int address) { + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(sDataOpNames.OrgDirective), + SourceFormatter.FormatHexValue(address, 4), string.Empty); + } + + // IGenerator + public void OutputRegWidthDirective(int offset, int prevM, int prevX, int newM, int newX) { + // prevM/prevX may be ambiguous for offset 0, but otherwise everything + // should be either 0 or 1. + Debug.Assert(newM == 0 || newM == 1); + Debug.Assert(newX == 0 || newX == 1); + + if (offset == 0 && newM == 1 && newX == 1) { + // Assembler defaults to short regs, so we can skip this. + return; + } + OutputLine(string.Empty, + SourceFormatter.FormatPseudoOp(sDataOpNames.RegWidthDirective), + "%" + newM + newX, string.Empty); + } + + // IGenerator + public void OutputLine(string fullLine) { + mOutStream.WriteLine(fullLine); + } + + // IGenerator + public void OutputLine(string label, string opcode, string operand, string comment) { + // Split long label, but not on EQU directives (confuses the assembler). + if (mLongLabelNewLine && label.Length >= mColumnWidths[0] && + !string.Equals(opcode, sDataOpNames.EquDirective, + StringComparison.InvariantCultureIgnoreCase)) { + mOutStream.WriteLine(label); + label = string.Empty; + } + + mLineBuilder.Clear(); + TextUtil.AppendPaddedString(mLineBuilder, label, mColumnWidths[0]); + TextUtil.AppendPaddedString(mLineBuilder, opcode, mColumnWidths[0] + mColumnWidths[1]); + TextUtil.AppendPaddedString(mLineBuilder, operand, + mColumnWidths[0] + mColumnWidths[1] + mColumnWidths[2]); + if (string.IsNullOrEmpty(comment)) { + // Trim trailing spaces off of opcode or operand. If they want trailing + // spaces at the end of a comment, that's fine. + CommonUtil.TextUtil.TrimEnd(mLineBuilder); + } else { + mLineBuilder.Append(comment); + } + + mOutStream.WriteLine(mLineBuilder.ToString()); + } + + + private enum RevMode { Forward, Reverse, BlockReverse }; + + private void OutputString(int offset, string labelStr, string commentStr) { + // This gets complicated. + // + // For Dci, L8String, and L16String, the entire string needs to fit in the + // operand of one line. If it can't, we need to separate the length byte/word + // or inverted character out, and just dump the rest as ASCII. Computing the + // line length requires factoring delimiter character escapes. (NOTE: contrary + // to the documentation, STR and STRL do include trailing hex characters in the + // length calculation, so it's possible to escape delimiters.) + // + // For Reverse, we can span lines, but only if we emit the lines in + // backward order. Also, Merlin doesn't allow hex to be embedded in a REV + // operation, so we can't use REV if the string contains a delimiter. + // + // DciReverse is deprecated, but we can handle it as a Reverse string with a + // trailing byte on a following line. + // + // For aesthetic purposes, zero-length CString, L8String, and L16String + // should be output as DFB/DW zeroes rather than an empty string -- makes + // it easier to read. + + Formatter formatter = SourceFormatter; + byte[] data = Project.FileData; + Anattrib attr = Project.GetAnattrib(offset); + FormatDescriptor dfd = attr.DataDescriptor; + Debug.Assert(dfd != null); + Debug.Assert(dfd.FormatType == FormatDescriptor.Type.String); + Debug.Assert(dfd.Length > 0); + + bool highAscii = false; + int showZeroes = 0; + int leadingBytes = 0; + int trailingBytes = 0; + bool showLeading = false; + bool showTrailing = false; + RevMode revMode = RevMode.Forward; + + switch (dfd.FormatSubType) { + case FormatDescriptor.SubType.None: + highAscii = (data[offset] & 0x80) != 0; + break; + case FormatDescriptor.SubType.Dci: + highAscii = (data[offset] & 0x80) != 0; + break; + case FormatDescriptor.SubType.Reverse: + highAscii = (data[offset] & 0x80) != 0; + revMode = RevMode.Reverse; + break; + case FormatDescriptor.SubType.DciReverse: + highAscii = (data[offset + dfd.Length - 1] & 0x80) != 0; + revMode = RevMode.Reverse; + break; + case FormatDescriptor.SubType.CString: + highAscii = (data[offset] & 0x80) != 0; + if (dfd.Length == 1) { + showZeroes = 1; // empty null-terminated string + } + trailingBytes = 1; + showTrailing = true; + break; + case FormatDescriptor.SubType.L8String: + if (dfd.Length > 1) { + highAscii = (data[offset + 1] & 0x80) != 0; + } else { + //showZeroes = 1; + } + leadingBytes = 1; + break; + case FormatDescriptor.SubType.L16String: + if (dfd.Length > 2) { + highAscii = (data[offset + 2] & 0x80) != 0; + } else { + //showZeroes = 2; + } + leadingBytes = 2; + break; + default: + Debug.Assert(false); + return; + } + + if (showZeroes != 0) { + // Empty string. Just output the length byte(s) or null terminator. + GenerateShortSequence(offset, showZeroes, out string opcode, out string operand); + OutputLine(labelStr, opcode, operand, commentStr); + return; + } + + // Merlin 32 uses single-quote for low ASCII, double-quote for high ASCII. When + // quoting the delimiter we use a hexadecimal value. We need to bear in mind that + // we're forcing the characters to low ASCII, but the actual character being + // escaped might be in high ASCII. Hence delim vs. delimReplace. + char delim = highAscii ? '"' : '\''; + char delimReplace = highAscii ? ((char)(delim | 0x80)) : delim; + StringGather gath = null; + + // Run the string through so we can see if it'll fit on one line. As a minor + // optimization, we skip this step for "generic" strings, which are probably + // the most common thing. + if (dfd.FormatSubType != FormatDescriptor.SubType.None) { + gath = new StringGather(this, labelStr, "???", commentStr, delim, + delimReplace, StringGather.ByteStyle.DenseHex, MAX_OPERAND_LEN, true); + FeedGath(gath, data, offset, dfd.Length, revMode, leadingBytes, showLeading, + trailingBytes, showTrailing); + Debug.Assert(gath.NumLinesOutput > 0); + } + + string opcodeStr; + + switch (dfd.FormatSubType) { + case FormatDescriptor.SubType.None: + opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric; + break; + case FormatDescriptor.SubType.Dci: + if (gath.NumLinesOutput == 1) { + opcodeStr = highAscii ? sDataOpNames.StrDciHi : sDataOpNames.StrDci; + } else { + opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric; + trailingBytes = 1; + showTrailing = true; + } + break; + case FormatDescriptor.SubType.Reverse: + if (gath.HasDelimiter) { + // can't include escaped delimiters in REV + opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric; + revMode = RevMode.Forward; + } else if (gath.NumLinesOutput > 1) { + opcodeStr = highAscii ? sDataOpNames.StrReverseHi : sDataOpNames.StrReverse; + revMode = RevMode.BlockReverse; + } else { + opcodeStr = highAscii ? sDataOpNames.StrReverseHi : sDataOpNames.StrReverse; + Debug.Assert(revMode == RevMode.Reverse); + } + break; + case FormatDescriptor.SubType.DciReverse: + // Mostly punt -- output as ASCII with special handling for first byte. + opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric; + revMode = RevMode.Forward; + leadingBytes = 1; + showLeading = true; + break; + case FormatDescriptor.SubType.CString: + //opcodeStr = sDataOpNames.StrNullTerm[highAscii ? 1 : 0]; + opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric; + break; + case FormatDescriptor.SubType.L8String: + if (gath.NumLinesOutput == 1) { + opcodeStr = highAscii ? sDataOpNames.StrLen8Hi : sDataOpNames.StrLen8; + } else { + opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric; + leadingBytes = 1; + showLeading = true; + } + break; + case FormatDescriptor.SubType.L16String: + if (gath.NumLinesOutput == 1) { + opcodeStr = highAscii ? sDataOpNames.StrLen16Hi : sDataOpNames.StrLen16; + } else { + opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric; + leadingBytes = 2; + showLeading = true; + } + break; + default: + Debug.Assert(false); + return; + } + + opcodeStr = formatter.FormatPseudoOp(opcodeStr); + + // Create a new StringGather, with the final opcode choice. + gath = new StringGather(this, labelStr, opcodeStr, commentStr, delim, + delimReplace, StringGather.ByteStyle.DenseHex, MAX_OPERAND_LEN, false); + FeedGath(gath, data, offset, dfd.Length, revMode, leadingBytes, showLeading, + trailingBytes, showTrailing); + } + + /// + /// Feeds the bytes into the StringGather. + /// + private void FeedGath(StringGather gath, byte[] data, int offset, int length, + RevMode revMode, int leadingBytes, bool showLeading, + int trailingBytes, bool showTrailing) { + int startOffset = offset; + int strEndOffset = offset + length - trailingBytes; + + if (showLeading) { + while (leadingBytes-- > 0) { + gath.WriteByte(data[offset++]); + } + } else { + offset += leadingBytes; + } + if (revMode == RevMode.BlockReverse) { + const int maxPerLine = MAX_OPERAND_LEN - 2; + int numBlockLines = (length + maxPerLine - 1) / maxPerLine; + + for (int chunk = 0; chunk < numBlockLines; chunk++) { + int chunkOffset = startOffset + chunk * maxPerLine; + int endOffset = chunkOffset + maxPerLine; + if (endOffset > strEndOffset) { + endOffset = strEndOffset; + } + for (int off = endOffset - 1; off >= chunkOffset; off--) { + gath.WriteChar((char)(data[off] & 0x7f)); + } + } + } else { + for (; offset < strEndOffset; offset++) { + if (revMode == RevMode.Forward) { + gath.WriteChar((char)(data[offset] & 0x7f)); + } else if (revMode == RevMode.Reverse) { + int posn = startOffset + (strEndOffset - offset) - 1; + gath.WriteChar((char)(data[posn] & 0x7f)); + } else { + Debug.Assert(false); + } + } + } + while (showTrailing && trailingBytes-- > 0) { + gath.WriteByte(data[offset++]); + } + gath.Finish(); + } + } + + #endregion IGenerator + + + #region IAssembler + + /// + /// Cross-assembler execution interface. + /// + public class AsmMerlin32 : IAssembler { + // Paths from generator. + private List mPathNames; + + // Directory to make current before executing assembler. + private string mWorkDirectory; + + + // IAssembler + public void GetExeIdentifiers(out string humanName, out string exeName) { + humanName = "Merlin Assembler"; + exeName = "Merlin32"; + } + + // IAssembler + public AssemblerConfig GetDefaultConfig() { + return new AssemblerConfig(string.Empty, new int[] { 9, 6, 11, 74 }); + } + + // IAssembler + public AssemblerVersion QueryVersion() { + AssemblerConfig config = + AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Merlin32); + if (config == null || string.IsNullOrEmpty(config.ExecutablePath)) { + return null; + } + + ShellCommand cmd = new ShellCommand(config.ExecutablePath, string.Empty, + Directory.GetCurrentDirectory(), null); + cmd.Execute(); + if (string.IsNullOrEmpty(cmd.Stdout)) { + return null; + } + + // Stdout: "C:\Src\WorkBench\Merlin32.exe v 1.0, (c) Brutal Deluxe ..." + // Other platforms may not have the ".exe". Find first occurrence of " v ". + + const string PREFIX = " v "; // not expecting this to appear in the path + string str = cmd.Stdout; + int start = str.IndexOf(PREFIX); + int end = (start < 0) ? -1 : str.IndexOf(',', start); + + if (start < 0 || end < 0 || start + PREFIX.Length >= end) { + Debug.WriteLine("Couldn't find version in " + str); + return null; + } + start += PREFIX.Length; + string versionStr = str.Substring(start, end - start); + CommonUtil.Version version = CommonUtil.Version.Parse(versionStr); + if (!version.IsValid) { + return null; + } + return new AssemblerVersion(versionStr, version); + } + + // IAssembler + public void Configure(List pathNames, string workDirectory) { + // Clone pathNames, in case the caller decides to modify the original. + mPathNames = new List(pathNames.Count); + foreach (string str in pathNames) { + mPathNames.Add(str); + } + + mWorkDirectory = workDirectory; + } + + // IAssembler + public AssemblerResults RunAssembler(BackgroundWorker worker) { + // Reduce input file to a partial path if possible. This is really just to make + // what we display to the user a little easier to read. + string pathName = mPathNames[0]; + if (pathName.StartsWith(mWorkDirectory)) { + pathName = pathName.Remove(0, mWorkDirectory.Length + 1); + } else { + // Unexpected, but shouldn't be a problem. + Debug.WriteLine("NOTE: source file is not in work directory"); + } + + AssemblerConfig config = + AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Merlin32); + if (string.IsNullOrEmpty(config.ExecutablePath)) { + Debug.WriteLine("Assembler not configured"); + return null; + } + + worker.ReportProgress(0, Res.Strings.PROGRESS_ASSEMBLING); + + // Wrap pathname in quotes in case it has spaces. + // (Do we need to shell-escape quotes in the pathName?) + // + // Merlin 32 has no options. The second argument is the macro include file path. + ShellCommand cmd = new ShellCommand(config.ExecutablePath, ". \"" + pathName + "\"", + mWorkDirectory, null); + cmd.Execute(); + + // Can't really do anything with a "cancel" request. + + // Output filename is the input filename without the ".S". Since the filename + // was generated by us we can be confident in the format. + string outputFile = mPathNames[0].Substring(0, mPathNames[0].Length - 2); + + return new AssemblerResults(cmd.FullCommandLine, cmd.ExitCode, cmd.Stdout, + cmd.Stderr, outputFile); + } + } + + #endregion IAssembler +} diff --git a/SourceGenWPF/AsmGen/AsmTass64.cs b/SourceGenWPF/AsmGen/AsmTass64.cs new file mode 100644 index 0000000..0e8af7f --- /dev/null +++ b/SourceGenWPF/AsmGen/AsmTass64.cs @@ -0,0 +1,778 @@ +/* + * 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.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Text; + +using Asm65; +using CommonUtil; + +namespace SourceGenWPF.AsmGen { + #region IGenerator + + /// + /// Generate source code compatible with the 64tass assembler + /// (https://sourceforge.net/projects/tass64/). + /// + /// The assembler is officially called "64tass", but it's sometimes written "tass64" because + /// in some cases you can't start an identifier with a number. + /// + /// We need to deal with a couple of unusual aspects: + /// (1) The prefix for a local label is '_', which is generally a legal character. So + /// if somebody creates a label with a leading '_', and it's not actually local, we have + /// to "de-local" it somehow. + /// (2) By default, labels are handled in a case-insensitive fashion, which is extremely + /// rare for programming languages. Case sensitivity can be enabled with the "-C" flag. + /// Anybody who wants to assemble the generated code will need to be aware of this. + /// + public class GenTass64 : IGenerator { + private const string ASM_FILE_SUFFIX = "_64tass.S"; // must start with underscore + private const int MAX_OPERAND_LEN = 64; + + // IGenerator + public DisasmProject Project { get; private set; } + + // IGenerator + public Formatter SourceFormatter { get; private set; } + + // IGenerator + public AppSettings Settings { get; private set; } + + // IGenerator + public AssemblerQuirks Quirks { get; private set; } + + // IGenerator + public LabelLocalizer Localizer { get { return mLocalizer; } } + + /// + /// Working directory, i.e. where we write our output file(s). + /// + private string mWorkDirectory; + + /// + /// If set, long labels get their own line. + /// + private bool mLongLabelNewLine; + + /// + /// Output column widths. + /// + private int[] mColumnWidths; + + /// + /// Base filename. Typically the project file name without the ".dis65" extension. + /// + private string mFileNameBase; + + /// + /// StringBuilder to use when composing a line. Held here to reduce allocations. + /// + private StringBuilder mLineBuilder = new StringBuilder(100); + + /// + /// Label localization helper. + /// + private LabelLocalizer mLocalizer; + + /// + /// Stream to send the output to. + /// + private StreamWriter mOutStream; + + /// + /// If we output a ".logical", we will need a ".here" eventually. + /// + private bool mNeedHereOp; + + /// + /// Holds detected version of configured assembler. + /// + private CommonUtil.Version mAsmVersion = CommonUtil.Version.NO_VERSION; + + // Version we're coded against. + private static CommonUtil.Version V1_53 = new CommonUtil.Version(1, 53, 1515); + + + // Pseudo-op string constants. + private static PseudoOp.PseudoOpNames sDataOpNames = new PseudoOp.PseudoOpNames() { + EquDirective = "=", + OrgDirective = ".logical", + //RegWidthDirective // .as, .al, .xs, .xl + DefineData1 = ".byte", + DefineData2 = ".word", + DefineData3 = ".long", + DefineData4 = ".dword", + //DefineBigData2 + //DefineBigData3 + //DefineBigData4 + Fill = ".fill", + //Dense // no equivalent, use .byte with comma-separated args + StrGeneric = ".text", + //StrReverse + StrNullTerm = ".null", + StrLen8 = ".ptext", + //StrLen16 + //StrDci + //StrDciReverse + }; + private const string HERE_PSEUDO_OP = ".here"; + + + // IGenerator + public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps, + out Formatter.FormatConfig formatConfig) { + pseudoOps = sDataOpNames; + + formatConfig = new Formatter.FormatConfig(); + SetFormatConfigValues(ref formatConfig); + } + + // IGenerator + public void Configure(DisasmProject project, string workDirectory, string fileNameBase, + AssemblerVersion asmVersion, AppSettings settings) { + Debug.Assert(project != null); + Debug.Assert(!string.IsNullOrEmpty(workDirectory)); + Debug.Assert(!string.IsNullOrEmpty(fileNameBase)); + + Project = project; + Quirks = new AssemblerQuirks(); + + mWorkDirectory = workDirectory; + mFileNameBase = fileNameBase; + Settings = settings; + + mLongLabelNewLine = Settings.GetBool(AppSettings.SRCGEN_LONG_LABEL_NEW_LINE, false); + + AssemblerConfig config = AssemblerConfig.GetConfig(settings, + AssemblerInfo.Id.Tass64); + mColumnWidths = (int[])config.ColumnWidths.Clone(); + } + + /// + /// Configures the assembler-specific format items. + /// + private void SetFormatConfigValues(ref Formatter.FormatConfig config) { + // Must be lower case when --case-sensitive is used. + config.mUpperOpcodes = false; + config.mUpperPseudoOpcodes = false; + config.mUpperOperandA = false; + config.mUpperOperandS = false; + config.mUpperOperandXY = false; + + config.mBankSelectBackQuote = true; + + config.mForceAbsOpcodeSuffix = string.Empty; + config.mForceLongOpcodeSuffix = string.Empty; + config.mForceDirectOperandPrefix = string.Empty; + config.mForceAbsOperandPrefix = "@w"; // word + config.mForceLongOperandPrefix = "@l"; // long + config.mEndOfLineCommentDelimiter = ";"; + config.mFullLineCommentDelimiterBase = ";"; + config.mBoxLineCommentDelimiter = ";"; + config.mAllowHighAsciiCharConst = false; + config.mExpressionMode = Formatter.FormatConfig.ExpressionMode.Common; + } + + // IGenerator + public List GenerateSource(BackgroundWorker worker) { + List pathNames = new List(1); + + string fileName = mFileNameBase + ASM_FILE_SUFFIX; + string pathName = Path.Combine(mWorkDirectory, fileName); + pathNames.Add(pathName); + + Formatter.FormatConfig config = new Formatter.FormatConfig(); + GenCommon.ConfigureFormatterFromSettings(Settings, ref config); + SetFormatConfigValues(ref config); + SourceFormatter = new Formatter(config); + + string msg = string.Format(Res.Strings.PROGRESS_GENERATING_FMT, pathName); + worker.ReportProgress(0, msg); + + mLocalizer = new LabelLocalizer(Project); + if (!Settings.GetBool(AppSettings.SRCGEN_DISABLE_LABEL_LOCALIZATION, false)) { + mLocalizer.LocalPrefix = "_"; + mLocalizer.Analyze(); + } + mLocalizer.MaskLeadingUnderscores(); + + // Use UTF-8 encoding, without a byte-order mark. + using (StreamWriter sw = new StreamWriter(pathName, false, new UTF8Encoding(false))) { + mOutStream = sw; + + if (Settings.GetBool(AppSettings.SRCGEN_ADD_IDENT_COMMENT, false)) { + OutputLine(SourceFormatter.FullLineCommentDelimiter + + string.Format(Res.Strings.GENERATED_FOR_VERSION_FMT, + "64tass", V1_53, AsmTass64.OPTIONS)); + } + + GenCommon.Generate(this, sw, worker); + + if (mNeedHereOp) { + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(HERE_PSEUDO_OP), + string.Empty, string.Empty); + } + } + mOutStream = null; + + return pathNames; + } + + // IGenerator + public void OutputAsmConfig() { + CpuDef cpuDef = Project.CpuDef; + string cpuStr; + if (cpuDef.Type == CpuDef.CpuType.Cpu65816) { + cpuStr = "65816"; + } else if (cpuDef.Type == CpuDef.CpuType.Cpu65C02) { + cpuStr = "65c02"; + } else if (cpuDef.Type == CpuDef.CpuType.Cpu6502 && cpuDef.HasUndocumented) { + cpuStr = "6502i"; + } else { + cpuStr = "6502"; + } + + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(".cpu"), + '\"' + cpuStr + '\"', string.Empty); + } + + // IGenerator + public string ModifyOpcode(int offset, OpDef op) { + if (op.IsUndocumented) { + if (Project.CpuDef.Type == CpuDef.CpuType.Cpu65C02) { + // none of the "LDD" stuff is handled + return null; + } + if ((op.Mnemonic == OpName.ANC && op.Opcode != 0x0b) || + (op.Mnemonic == OpName.JAM && op.Opcode != 0x02)) { + // There are multiple opcodes that match the mnemonic. Output the + // mnemonic for the first one and hex for the rest. + return null; + } else if (op.Mnemonic == OpName.NOP || op.Mnemonic == OpName.DOP || + op.Mnemonic == OpName.TOP) { + // the various undocumented no-ops aren't handled + return null; + } else if (op.Mnemonic == OpName.SBC) { + // this is the alternate reference to SBC + return null; + } else if (op == OpDef.OpSHA_DPIndIndexY) { + // not recognized ($93) + return null; + } + } + if (op == OpDef.OpBRK_StackInt || op == OpDef.OpCOP_StackInt || + op == OpDef.OpWDM_WDM) { + // 64tass doesn't like these to have an operand. Output as hex. + return null; + } + return string.Empty; // indicate original is fine + } + + // IGenerator + public void GenerateShortSequence(int offset, int length, out string opcode, + out string operand) { + Debug.Assert(length >= 1 && length <= 4); + + // Use a comma-separated list of individual hex bytes. + opcode = sDataOpNames.DefineData1; + + StringBuilder sb = new StringBuilder(length * 4); + for (int i = 0; i < length; i++) { + if (i != 0) { + sb.Append(','); + } + sb.Append(SourceFormatter.FormatHexValue(Project.FileData[offset + i], 2)); + } + operand = sb.ToString(); + } + + // IGenerator + public void OutputDataOp(int offset) { + Formatter formatter = SourceFormatter; + byte[] data = Project.FileData; + Anattrib attr = Project.GetAnattrib(offset); + + string labelStr = string.Empty; + if (attr.Symbol != null) { + labelStr = mLocalizer.ConvLabel(attr.Symbol.Label); + } + + string commentStr = SourceFormatter.FormatEolComment(Project.Comments[offset]); + string opcodeStr, operandStr; + + FormatDescriptor dfd = attr.DataDescriptor; + Debug.Assert(dfd != null); + int length = dfd.Length; + Debug.Assert(length > 0); + + bool multiLine = false; + switch (dfd.FormatType) { + case FormatDescriptor.Type.Default: + if (length != 1) { + Debug.Assert(false); + length = 1; + } + opcodeStr = sDataOpNames.DefineData1; + int operand = RawData.GetWord(data, offset, length, false); + operandStr = formatter.FormatHexValue(operand, length * 2); + break; + case FormatDescriptor.Type.NumericLE: + opcodeStr = sDataOpNames.GetDefineData(length); + operand = RawData.GetWord(data, offset, length, false); + operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable, + mLocalizer.LabelMap, dfd, operand, length, + PseudoOp.FormatNumericOpFlags.None); + break; + case FormatDescriptor.Type.NumericBE: + opcodeStr = sDataOpNames.GetDefineBigData(length); + if (opcodeStr == null) { + // Nothing defined, output as comma-separated single-byte values. + GenerateShortSequence(offset, length, out opcodeStr, out operandStr); + } else { + operand = RawData.GetWord(data, offset, length, true); + operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable, + mLocalizer.LabelMap, dfd, operand, length, + PseudoOp.FormatNumericOpFlags.None); + } + break; + case FormatDescriptor.Type.Fill: + opcodeStr = sDataOpNames.Fill; + operandStr = length + "," + formatter.FormatHexValue(data[offset], 2); + break; + case FormatDescriptor.Type.Dense: + multiLine = true; + opcodeStr = operandStr = null; + OutputDenseHex(offset, length, labelStr, commentStr); + break; + case FormatDescriptor.Type.String: + multiLine = true; + opcodeStr = operandStr = null; + OutputString(offset, labelStr, commentStr); + break; + default: + opcodeStr = "???"; + operandStr = "***"; + break; + } + + if (!multiLine) { + opcodeStr = formatter.FormatPseudoOp(opcodeStr); + OutputLine(labelStr, opcodeStr, operandStr, commentStr); + } + } + + private void OutputDenseHex(int offset, int length, string labelStr, string commentStr) { + Formatter formatter = SourceFormatter; + byte[] data = Project.FileData; + StringBuilder sb = new StringBuilder(MAX_OPERAND_LEN); + + string opcodeStr = formatter.FormatPseudoOp(sDataOpNames.DefineData1); + + int maxPerLine = MAX_OPERAND_LEN / 4; + int numChunks = (length + maxPerLine - 1) / maxPerLine; + for (int chunk = 0; chunk < numChunks; chunk++) { + int chunkStart = chunk * maxPerLine; + int chunkEnd = Math.Min((chunk + 1) * maxPerLine, length); + for (int i = chunkStart; i < chunkEnd; i++) { + if (i != chunkStart) { + sb.Append(','); + } + sb.Append(formatter.FormatHexValue(data[offset + i], 2)); + } + + OutputLine(labelStr, opcodeStr, sb.ToString(), commentStr); + labelStr = commentStr = string.Empty; + sb.Clear(); + } + } + + /// + /// Outputs formatted data in an unformatted way, because the code generator couldn't + /// figure out how to do something better. + /// + private void OutputNoJoy(int offset, int length, string labelStr, string commentStr) { + byte[] data = Project.FileData; + Debug.Assert(length > 0); + Debug.Assert(offset >= 0 && offset < data.Length); + + bool singleValue = true; + byte val = data[offset]; + for (int i = 1; i < length; i++) { + if (data[offset + i] != val) { + singleValue = false; + break; + } + } + + if (singleValue) { + string opcodeStr = SourceFormatter.FormatPseudoOp(sDataOpNames.Fill); + string operandStr = length + "," + SourceFormatter.FormatHexValue(val, 2); + OutputLine(labelStr, opcodeStr, operandStr, commentStr); + } else { + OutputDenseHex(offset, length, labelStr, commentStr); + } + } + + // IGenerator + public void OutputEquDirective(string name, string valueStr, string comment) { + OutputLine(name, SourceFormatter.FormatPseudoOp(sDataOpNames.EquDirective), + valueStr, SourceFormatter.FormatEolComment(comment)); + } + + // IGenerator + public void OutputOrgDirective(int offset, int address) { + // 64tass separates the "compile offset", which determines where the output fits + // into the generated binary, and "program counter", which determines the code + // the assembler generates. Since we need to explicitly specify every byte in + // the output file, the compile offset isn't very useful. We want to set it once + // before the first line of code, then leave it alone. + // + // Any subsequent ORG changes are made to the program counter, and take the form + // of a pair of ops (.logical to open, .here to end). Omitting the .here + // causes an error. + if (offset == 0) { + // Set the "compile offset" to the initial address. + OutputLine("*", "=", SourceFormatter.FormatHexValue(Project.AddrMap.Get(0), 4), + string.Empty); + } else { + if (mNeedHereOp) { + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(HERE_PSEUDO_OP), + string.Empty, string.Empty); + } + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(sDataOpNames.OrgDirective), + SourceFormatter.FormatHexValue(address, 4), string.Empty); + mNeedHereOp = true; + } + } + + // IGenerator + public void OutputRegWidthDirective(int offset, int prevM, int prevX, int newM, int newX) { + if (prevM != newM) { + string mop = (newM == 0) ? ".al" : ".as"; + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(mop), + string.Empty, string.Empty); + } + if (prevX != newX) { + string xop = (newX == 0) ? ".xl" : ".xs"; + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(xop), + string.Empty, string.Empty); + } + } + + // IGenerator + public void OutputLine(string fullLine) { + mOutStream.WriteLine(fullLine); + } + + // IGenerator + public void OutputLine(string label, string opcode, string operand, string comment) { + // Break the line if the label is long and it's not a .EQ directive. + if (!string.IsNullOrEmpty(label) && + !string.Equals(opcode, sDataOpNames.EquDirective, + StringComparison.InvariantCultureIgnoreCase)) { + + if (mLongLabelNewLine && label.Length >= mColumnWidths[0]) { + mOutStream.WriteLine(label); + label = string.Empty; + } + } + + mLineBuilder.Clear(); + TextUtil.AppendPaddedString(mLineBuilder, label, mColumnWidths[0]); + TextUtil.AppendPaddedString(mLineBuilder, opcode, mColumnWidths[0] + mColumnWidths[1]); + TextUtil.AppendPaddedString(mLineBuilder, operand, + mColumnWidths[0] + mColumnWidths[1] + mColumnWidths[2]); + if (string.IsNullOrEmpty(comment)) { + // Trim trailing spaces off of opcode or operand. If they want trailing + // spaces at the end of a comment, that's fine. + CommonUtil.TextUtil.TrimEnd(mLineBuilder); + } else { + mLineBuilder.Append(comment); + } + + mOutStream.WriteLine(mLineBuilder.ToString()); + } + + private void OutputString(int offset, string labelStr, string commentStr) { + // Normal ASCII strings are handled with a simple .text directive. + // + // CString and L8String have directives (.null, .ptext), but we can only use + // them if the string fits on one line and doesn't include delimiters. + // + // We could probably do something fancy with the character encoding options to + // make high-ASCII work nicely. + // + // We might be able to define a macro for DCI and Reverse. + + Formatter formatter = SourceFormatter; + byte[] data = Project.FileData; + Anattrib attr = Project.GetAnattrib(offset); + FormatDescriptor dfd = attr.DataDescriptor; + Debug.Assert(dfd != null); + Debug.Assert(dfd.FormatType == FormatDescriptor.Type.String); + Debug.Assert(dfd.Length > 0); + + bool highAscii = false; + int leadingBytes = 0; + int trailingBytes = 0; + bool showLeading = false; + bool showTrailing = false; + + switch (dfd.FormatSubType) { + case FormatDescriptor.SubType.None: + highAscii = (data[offset] & 0x80) != 0; + break; + case FormatDescriptor.SubType.Dci: + highAscii = (data[offset] & 0x80) != 0; + trailingBytes = 1; + showTrailing = true; + break; + case FormatDescriptor.SubType.Reverse: + highAscii = (data[offset] & 0x80) != 0; + break; + case FormatDescriptor.SubType.DciReverse: + highAscii = (data[offset + dfd.Length - 1] & 0x80) != 0; + leadingBytes = 1; + showLeading = true; + break; + case FormatDescriptor.SubType.CString: + highAscii = (data[offset] & 0x80) != 0; + trailingBytes = 1; + showTrailing = true; + break; + case FormatDescriptor.SubType.L8String: + if (dfd.Length > 1) { + highAscii = (data[offset + 1] & 0x80) != 0; + } + leadingBytes = 1; + showLeading = true; + break; + case FormatDescriptor.SubType.L16String: + if (dfd.Length > 2) { + highAscii = (data[offset + 2] & 0x80) != 0; + } + leadingBytes = 2; + showLeading = true; + break; + default: + Debug.Assert(false); + return; + } + + char delim = '"'; + StringGather gath = null; + + // Run the string through so we can see if it'll fit on one line. As a minor + // optimization, we skip this step for "generic" strings, which are probably + // the most common thing. + if (dfd.FormatSubType != FormatDescriptor.SubType.None || highAscii) { + gath = new StringGather(this, labelStr, "???", commentStr, delim, + delim, StringGather.ByteStyle.CommaSep, MAX_OPERAND_LEN, true); + FeedGath(gath, data, offset, dfd.Length, leadingBytes, showLeading, + trailingBytes, showTrailing); + Debug.Assert(gath.NumLinesOutput > 0); + } + + string opcodeStr = formatter.FormatPseudoOp(sDataOpNames.StrGeneric); + + switch (dfd.FormatSubType) { + case FormatDescriptor.SubType.None: + // TODO(someday): something fancy with encodings to handle high-ASCII text? + break; + case FormatDescriptor.SubType.Dci: + case FormatDescriptor.SubType.Reverse: + case FormatDescriptor.SubType.DciReverse: + // Fully configured above. + break; + case FormatDescriptor.SubType.CString: + if (gath.NumLinesOutput == 1 && !gath.HasDelimiter) { + opcodeStr = sDataOpNames.StrNullTerm; + showTrailing = false; + } + break; + case FormatDescriptor.SubType.L8String: + if (gath.NumLinesOutput == 1 && !gath.HasDelimiter) { + opcodeStr = sDataOpNames.StrLen8; + showLeading = false; + } + break; + case FormatDescriptor.SubType.L16String: + // Implement as macro? + break; + default: + Debug.Assert(false); + return; + } + + if (highAscii) { + OutputNoJoy(offset, dfd.Length, labelStr, commentStr); + return; + } + + // Create a new StringGather, with the final opcode choice. + gath = new StringGather(this, labelStr, opcodeStr, commentStr, delim, + delim, StringGather.ByteStyle.CommaSep, MAX_OPERAND_LEN, false); + FeedGath(gath, data, offset, dfd.Length, leadingBytes, showLeading, + trailingBytes, showTrailing); + } + + /// + /// Feeds the bytes into the StringGather. + /// + private void FeedGath(StringGather gath, byte[] data, int offset, int length, + int leadingBytes, bool showLeading, int trailingBytes, bool showTrailing) { + int startOffset = offset; + int strEndOffset = offset + length - trailingBytes; + + if (showLeading) { + while (leadingBytes-- > 0) { + gath.WriteByte(data[offset++]); + } + } else { + offset += leadingBytes; + } + for (; offset < strEndOffset; offset++) { + gath.WriteChar((char)(data[offset] & 0x7f)); + } + while (showTrailing && trailingBytes-- > 0) { + gath.WriteByte(data[offset++]); + } + gath.Finish(); + } + } + + #endregion IGenerator + + + #region IAssembler + + /// + /// Cross-assembler execution interface. + /// + public class AsmTass64 : IAssembler { + public const string OPTIONS = "--case-sensitive --nostart --long-address -Wall"; + + // Paths from generator. + private List mPathNames; + + // Directory to make current before executing assembler. + private string mWorkDirectory; + + + // IAssembler + public void GetExeIdentifiers(out string humanName, out string exeName) { + humanName = "64tass Assembler"; + exeName = "64tass"; + } + + // IAssembler + public AssemblerConfig GetDefaultConfig() { + return new AssemblerConfig(string.Empty, new int[] { 8, 8, 11, 73 }); + } + + // IAssembler + public AssemblerVersion QueryVersion() { + AssemblerConfig config = + AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Tass64); + if (config == null || string.IsNullOrEmpty(config.ExecutablePath)) { + return null; + } + + ShellCommand cmd = new ShellCommand(config.ExecutablePath, "--version", + Directory.GetCurrentDirectory(), null); + cmd.Execute(); + if (string.IsNullOrEmpty(cmd.Stdout)) { + return null; + } + + // Windows - Stdout: "64tass Turbo Assembler Macro V1.53.1515\r\n" + // Linux - Stdout: "64tass Turbo Assembler Macro V1.53.1515?\n" + + const string PREFIX = "Macro V"; + string str = cmd.Stdout; + int start = str.IndexOf(PREFIX); + int end = (start < 0) ? -1 : str.IndexOfAny(new char[] { '?', '\r', '\n' }, start + 1); + + if (start < 0 || end < 0 || start + PREFIX.Length >= end) { + Debug.WriteLine("Couldn't find version in " + str); + return null; + } + start += PREFIX.Length; + string versionStr = str.Substring(start, end - start); + CommonUtil.Version version = CommonUtil.Version.Parse(versionStr); + if (!version.IsValid) { + return null; + } + return new AssemblerVersion(versionStr, version); + } + + // IAssembler + public void Configure(List pathNames, string workDirectory) { + // Clone pathNames, in case the caller decides to modify the original. + mPathNames = new List(pathNames.Count); + foreach (string str in pathNames) { + mPathNames.Add(str); + } + + mWorkDirectory = workDirectory; + } + + // IAssembler + public AssemblerResults RunAssembler(BackgroundWorker worker) { + // Reduce input file to a partial path if possible. This is really just to make + // what we display to the user a little easier to read. + string pathName = mPathNames[0]; + if (pathName.StartsWith(mWorkDirectory)) { + pathName = pathName.Remove(0, mWorkDirectory.Length + 1); + } else { + // Unexpected, but shouldn't be a problem. + Debug.WriteLine("NOTE: source file is not in work directory"); + } + + AssemblerConfig config = + AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Tass64); + if (string.IsNullOrEmpty(config.ExecutablePath)) { + Debug.WriteLine("Assembler not configured"); + return null; + } + + worker.ReportProgress(0, Res.Strings.PROGRESS_ASSEMBLING); + + string outFileName = pathName.Substring(0, pathName.Length - 2); + + // Wrap pathname in quotes in case it has spaces. + // (Do we need to shell-escape quotes in the pathName?) + ShellCommand cmd = new ShellCommand(config.ExecutablePath, + OPTIONS + " \"" + pathName + "\"" + " -o \"" + outFileName + "\"", + mWorkDirectory, null); + cmd.Execute(); + + // Can't really do anything with a "cancel" request. + + // Output filename is the input filename without the ".S". Since the filename + // was generated by us we can be confident in the format. + string outputFile = mPathNames[0].Substring(0, mPathNames[0].Length - 2); + + return new AssemblerResults(cmd.FullCommandLine, cmd.ExitCode, cmd.Stdout, + cmd.Stderr, outputFile); + } + } + + #endregion IAssembler +} diff --git a/SourceGenWPF/AsmGen/AssemblerConfig.cs b/SourceGenWPF/AsmGen/AssemblerConfig.cs new file mode 100644 index 0000000..4dd2527 --- /dev/null +++ b/SourceGenWPF/AsmGen/AssemblerConfig.cs @@ -0,0 +1,112 @@ +/* + * 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.Diagnostics; +using System.Web.Script.Serialization; + +namespace SourceGenWPF.AsmGen { + /// + /// Assembler configuration holder. Serializes and deserializes information held in + /// application settings. + /// + public class AssemblerConfig { + // Public fields are deserialized from JSON. Changing the names will break compatibility. + + /// + /// Path to cross-assembler executable. Will be null or empty if this assembler + /// is not configured. + /// + public string ExecutablePath { get; set; } + + /// + /// Column display widths. + /// + public int[] ColumnWidths { get; set; } + + public const int NUM_COLUMNS = 4; // label, opcode, operand, comment + + + /// + /// Nullary constructor, for serialization. + /// + public AssemblerConfig() { } + + /// + /// Constructor. + /// + /// Path to executable. May be empty. + /// Column widths. + public AssemblerConfig(string exePath, int[] widths) { + if (exePath == null) { + throw new Exception("Bad exe path"); + } + if (widths.Length != NUM_COLUMNS) { + throw new Exception("Bad widths.Length " + widths.Length); + } + ExecutablePath = exePath; + ColumnWidths = widths; + } + + private static string GetSettingName(AssemblerInfo.Id id) { + return AppSettings.ASM_CONFIG_PREFIX + id.ToString(); + } + + /// + /// Creates a populated AssemblerConfig from the app settings for the specified ID. + /// If the assembler hasn't been configured yet, the default configuration object + /// will be returned. + /// + /// Settings object to pull the values from. + /// Assembler ID. + /// The AssemblerConfig. + public static AssemblerConfig GetConfig(AppSettings settings, AssemblerInfo.Id id) { + string cereal = settings.GetString(GetSettingName(id), null); + if (string.IsNullOrEmpty(cereal)) { + IAssembler asm = AssemblerInfo.GetAssembler(id); + return asm.GetDefaultConfig(); + } + + JavaScriptSerializer ser = new JavaScriptSerializer(); + try { + AssemblerConfig config = ser.Deserialize(cereal); + if (config.ColumnWidths == null || config.ColumnWidths.Length != NUM_COLUMNS) { + throw new Exception("Bad column widths"); + } + if (config.ExecutablePath == null) { + throw new Exception("Missing exe path"); + } + return config; + } catch (Exception ex) { + Debug.WriteLine("AssemblerConfig deserialization failed: " + ex.Message); + return null; + } + } + + /// + /// Updates the assembler settings for the specified ID. + /// + /// Settings object to update. + /// Assembler ID. + /// Asm configuration. + public static void SetConfig(AppSettings settings, AssemblerInfo.Id id, + AssemblerConfig config) { + JavaScriptSerializer ser = new JavaScriptSerializer(); + string cereal = ser.Serialize(config); + + settings.SetString(GetSettingName(id), cereal); + } + } +} diff --git a/SourceGenWPF/AsmGen/AssemblerInfo.cs b/SourceGenWPF/AsmGen/AssemblerInfo.cs new file mode 100644 index 0000000..6904195 --- /dev/null +++ b/SourceGenWPF/AsmGen/AssemblerInfo.cs @@ -0,0 +1,170 @@ +/* + * 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; +using System.Collections.Generic; +using System.Diagnostics; + +namespace SourceGenWPF.AsmGen { + /// + /// Static information on assemblers supported by SourceGen. This is relevant for both + /// assembly source generation and assembler execution. Nothing here is affected + /// by whether or not the assembler in question is actually installed. + /// + public class AssemblerInfo { + /// + /// Enumeration of supported assemblers. Alphabetical order looks nicest. + /// + public enum Id { + Unknown = 0, + Tass64, + Cc65, + Merlin32, + } + + /// + /// Static information for all known assemblers. + /// + /// The AsmType argument may be null. This is useful for non-cross assemblers. + /// + private static AssemblerInfo[] sInfo = new AssemblerInfo[] { + new AssemblerInfo(Id.Unknown, "???", null, null), + new AssemblerInfo(Id.Tass64, "64tass", typeof(GenTass64), typeof(AsmTass64)), + new AssemblerInfo(Id.Cc65, "cc65", typeof(GenCc65), typeof(AsmCc65)), + new AssemblerInfo(Id.Merlin32, "Merlin 32", typeof(GenMerlin32), typeof(AsmMerlin32)), + }; + + /// + /// Identifier. + /// + public Id AssemblerId { get; private set; } + + /// + /// Human-readable name. + /// + public string Name { get; private set; } + + /// + /// Type of generator class. + /// + public Type GenType { get; private set; } + + /// + /// Type of assembler class. + /// + public Type AsmType { get; private set; } + + + private AssemblerInfo(Id id, string name, Type genType, Type asmType) { + AssemblerId = id; + Name = name; + GenType = genType; + AsmType = asmType; + } + + /// + /// Returns an AssemblerInfo object for the specified id. + /// + /// Assembler identifier. + /// Reference to AssemblerInfo object. + public static AssemblerInfo GetAssemblerInfo(Id id) { + return sInfo[(int)id]; + } + + /// + /// Generator factory method. + /// + /// ID of assembler to return generator instance for. + /// New source generator object. + public static IGenerator GetGenerator(Id id) { + Type genType = sInfo[(int)id].GenType; + if (genType == null) { + Debug.Assert(false); // unexpected for generator + return null; + } else { + return (IGenerator)Activator.CreateInstance(genType); + } + } + + /// + /// Assembler factory method. + /// + /// ID of assembler to return assembler instance for. + /// New assembler interface object. + public static IAssembler GetAssembler(Id id) { + Type asmType = sInfo[(int)id].AsmType; + if (asmType == null) { + return null; + } else { + return (IAssembler)Activator.CreateInstance(asmType); + } + } + + /// + /// Provides a way to iterate through the set of known assemblers. This is probably + /// YAGNI -- we could just return the array -- but it would allow us to apply filters, + /// e.g. strip out assemblers that don't support 65816 code when that's the selected + /// CPU definition. + /// + private class AssemblerInfoIterator : IEnumerator { + private int mIndex = -1; + + public AssemblerInfo Current { + get { + if (mIndex < 0) { + // not started + return null; + } + return sInfo[mIndex]; + } + } + + object IEnumerator.Current { + get { + return Current; + } + } + + public void Dispose() { } + + public bool MoveNext() { + if (mIndex < 0) { + // skip element 0 (Unknown) + mIndex = 1; + } else { + mIndex++; + if (mIndex >= sInfo.Length) { + return false; + } + } + return true; + } + + public void Reset() { + mIndex = -1; + } + } + + public static IEnumerator GetInfoEnumerator() { + return new AssemblerInfoIterator(); + } + + + public override string ToString() { + return "Asm " + ((int)AssemblerId).ToString() + ": " + Name; + } + } +} diff --git a/SourceGenWPF/AsmGen/AssemblerVersion.cs b/SourceGenWPF/AsmGen/AssemblerVersion.cs new file mode 100644 index 0000000..2eb5ee0 --- /dev/null +++ b/SourceGenWPF/AsmGen/AssemblerVersion.cs @@ -0,0 +1,104 @@ +/* + * 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; + +namespace SourceGenWPF.AsmGen { + public class AssemblerVersion { + /// + /// Version string reported by the assembler. Retained mostly for debugging. + /// + public string VersionStr { get; private set; } + + /// + /// Version string converted to a Version object. For very complex version strings, + /// some information may be lost in the conversion. + /// + public CommonUtil.Version Version { get; private set; } + + //// Command pathname and modification date. Useful for caching values. + //private string ExeName { get; set; } + //private DateTime ExeModWhen { get; set; } + + public AssemblerVersion(string versionStr, CommonUtil.Version version) { + VersionStr = versionStr; + Version = version; + } + + public static AssemblerVersion GetVersion(AssemblerInfo.Id id) { + IAssembler asm = AssemblerInfo.GetAssembler(id); + if (asm == null) { + Debug.WriteLine("Assembler " + id + " not configured"); + return null; + } + return asm.QueryVersion(); + } + + public override string ToString() { + return "['" + VersionStr + "'/" + Version + "]"; + } + } + + /// + /// Maintains a cache of the versions of installed assemblers. + /// + public static class AssemblerVersionCache { + private static Dictionary sVersions = + new Dictionary(); + private static bool sQueried = false; + + /// + /// Queries the versions from all known assemblers, replacing any previously held data. + /// + public static void QueryVersions() { + IEnumerator iter = AssemblerInfo.GetInfoEnumerator(); + while (iter.MoveNext()) { + AssemblerInfo.Id id = iter.Current.AssemblerId; + if (id == AssemblerInfo.Id.Unknown) { + continue; + } + + AssemblerVersion vers = null; + IAssembler asm = AssemblerInfo.GetAssembler(id); + if (asm != null) { + vers = asm.QueryVersion(); + } + + Debug.WriteLine("Asm version query: " + id + "=" + vers); + sVersions[id] = vers; + } + + sQueried = true; + } + + /// + /// Returns the version information, or null if the query failed for this assembler. + /// + /// Assembler identifier. + /// Version info. + public static AssemblerVersion GetVersion(AssemblerInfo.Id id) { + if (!sQueried) { + QueryVersions(); + } + if (sVersions.TryGetValue(id, out AssemblerVersion vers)) { + return vers; + } else { + return null; + } + } + } +} diff --git a/SourceGenWPF/AsmGen/GenCommon.cs b/SourceGenWPF/AsmGen/GenCommon.cs new file mode 100644 index 0000000..8b7ed1c --- /dev/null +++ b/SourceGenWPF/AsmGen/GenCommon.cs @@ -0,0 +1,371 @@ +/* + * 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.ComponentModel; +using System.Diagnostics; +using System.IO; + +using Asm65; + +namespace SourceGenWPF.AsmGen { + public class GenCommon { + /// + /// Generates assembly source. + /// + /// This code is common to all generators. + /// + /// Reference to generator object (presumably the caller). + /// Text output sink. + /// Background worker object, for progress updates and + /// cancelation requests. + public static void Generate(IGenerator gen, StreamWriter sw, BackgroundWorker worker) { + DisasmProject proj = gen.Project; + Formatter formatter = gen.SourceFormatter; + int offset = 0; + + bool doAddCycles = gen.Settings.GetBool(AppSettings.SRCGEN_SHOW_CYCLE_COUNTS, false); + + GenerateHeader(gen, sw); + + // Used for M/X flag tracking. + StatusFlags prevFlags = StatusFlags.AllIndeterminate; + + int lastProgress = 0; + + while (offset < proj.FileData.Length) { + Anattrib attr = proj.GetAnattrib(offset); + + if (attr.IsInstructionStart && offset > 0 && + proj.GetAnattrib(offset - 1).IsData) { + // Transition from data to code. (Don't add blank line for inline data.) + gen.OutputLine(string.Empty); + } + + // Long comments come first. + if (proj.LongComments.TryGetValue(offset, out MultiLineComment longComment)) { + List formatted = longComment.FormatText(formatter, string.Empty); + foreach (string str in formatted) { + gen.OutputLine(str); + } + } + + // Check for address change. + int orgAddr = proj.AddrMap.Get(offset); + if (orgAddr >= 0) { + gen.OutputOrgDirective(offset, orgAddr); + } + + if (attr.IsInstructionStart) { + // Generate M/X reg width directive, if necessary. + // NOTE: we can suppress the initial directive if we know what the + // target assembler's default assumption is. Probably want to handle + // that in the ORG output handler. + if (proj.CpuDef.HasEmuFlag) { + StatusFlags curFlags = attr.StatusFlags; + curFlags.M = attr.StatusFlags.ShortM ? 1 : 0; + curFlags.X = attr.StatusFlags.ShortX ? 1 : 0; + if (curFlags.M != prevFlags.M || curFlags.X != prevFlags.X) { + // changed, output directive + gen.OutputRegWidthDirective(offset, prevFlags.M, prevFlags.X, + curFlags.M, curFlags.X); + } + + prevFlags = curFlags; + } + + // Look for embedded instructions. + int len; + for (len = 1; len < attr.Length; len++) { + if (proj.GetAnattrib(offset + len).IsInstructionStart) { + break; + } + } + + // Output instruction. + GenerateInstruction(gen, sw, offset, len, doAddCycles); + + if (attr.DoesNotContinue) { + gen.OutputLine(string.Empty); + } + + offset += len; + } else { + gen.OutputDataOp(offset); + offset += attr.Length; + } + + // Update progress meter. We don't want to spam it, so just ping it 10x. + int curProgress = (offset * 10) / proj.FileData.Length; + if (lastProgress != curProgress) { + if (worker.CancellationPending) { + Debug.WriteLine("GenCommon got cancellation request"); + return; + } + lastProgress = curProgress; + worker.ReportProgress(curProgress * 10); + //System.Threading.Thread.Sleep(500); + } + } + } + + private static void GenerateHeader(IGenerator gen, StreamWriter sw) { + DisasmProject proj = gen.Project; + Formatter formatter = gen.SourceFormatter; + + // Check for header comment. + if (proj.LongComments.TryGetValue(LineListGen.Line.HEADER_COMMENT_OFFSET, + out MultiLineComment headerComment)) { + List formatted = headerComment.FormatText(formatter, string.Empty); + foreach (string str in formatted) { + gen.OutputLine(str); + } + } + + gen.OutputAsmConfig(); + + // Format symbols. + foreach (DefSymbol defSym in proj.ActiveDefSymbolList) { + // Use an operand length of 1 so things are shown as concisely as possible. + string valueStr = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, + gen.Localizer.LabelMap, defSym.DataDescriptor, defSym.Value, 1, + PseudoOp.FormatNumericOpFlags.None); + gen.OutputEquDirective(defSym.Label, valueStr, defSym.Comment); + } + + // If there was at least one symbol, output a blank line. + if (proj.ActiveDefSymbolList.Count != 0) { + gen.OutputLine(string.Empty); + } + } + + private static void GenerateInstruction(IGenerator gen, StreamWriter sw, int offset, + int instrBytes, bool doAddCycles) { + DisasmProject proj = gen.Project; + Formatter formatter = gen.SourceFormatter; + byte[] data = proj.FileData; + Anattrib attr = proj.GetAnattrib(offset); + + string labelStr = string.Empty; + if (attr.Symbol != null) { + labelStr = gen.Localizer.ConvLabel(attr.Symbol.Label); + } + + OpDef op = proj.CpuDef.GetOpDef(data[offset]); + int operand = op.GetOperand(data, offset, attr.StatusFlags); + int instrLen = op.GetLength(attr.StatusFlags); + OpDef.WidthDisambiguation wdis = OpDef.WidthDisambiguation.None; + if (op.IsWidthPotentiallyAmbiguous) { + wdis = OpDef.GetWidthDisambiguation(instrLen, operand); + } + if (gen.Quirks.SinglePassAssembler && wdis == OpDef.WidthDisambiguation.None && + (op.AddrMode == OpDef.AddressMode.DP || + op.AddrMode == OpDef.AddressMode.DPIndexX) || + op.AddrMode == OpDef.AddressMode.DPIndexY) { + // Could be a forward reference to a direct-page label. + if (IsForwardLabelReference(gen, offset)) { + wdis = OpDef.WidthDisambiguation.ForceDirect; + } + } + + string opcodeStr = formatter.FormatOpcode(op, wdis); + + string formattedOperand = null; + int operandLen = instrLen - 1; + PseudoOp.FormatNumericOpFlags opFlags = PseudoOp.FormatNumericOpFlags.None; + bool isPcRelBankWrap = false; + + // Tweak branch instructions. We want to show the absolute address rather + // than the relative offset (which happens with the OperandAddress assignment + // below), and 1-byte branches should always appear as a 4-byte hex value. + if (op.AddrMode == OpDef.AddressMode.PCRel) { + Debug.Assert(attr.OperandAddress >= 0); + operandLen = 2; + opFlags = PseudoOp.FormatNumericOpFlags.IsPcRel; + } else if (op.AddrMode == OpDef.AddressMode.PCRelLong || + op.AddrMode == OpDef.AddressMode.StackPCRelLong) { + opFlags = PseudoOp.FormatNumericOpFlags.IsPcRel; + } else if (op.AddrMode == OpDef.AddressMode.Imm || + op.AddrMode == OpDef.AddressMode.ImmLongA || + op.AddrMode == OpDef.AddressMode.ImmLongXY) { + opFlags = PseudoOp.FormatNumericOpFlags.HasHashPrefix; + } + if (opFlags == PseudoOp.FormatNumericOpFlags.IsPcRel) { + int branchDist = attr.Address - attr.OperandAddress; + isPcRelBankWrap = branchDist > 32767 || branchDist < -32768; + } + + // 16-bit operands outside bank 0 need to include the bank when computing + // symbol adjustment. + int operandForSymbol = operand; + if (attr.OperandAddress >= 0) { + operandForSymbol = attr.OperandAddress; + } + + // Check Length to watch for bogus descriptors. (ApplyFormatDescriptors() should + // now be screening bad descriptors out, so we may not need the Length test.) + if (attr.DataDescriptor != null && attr.Length == attr.DataDescriptor.Length) { + // Format operand as directed. + if (op.AddrMode == OpDef.AddressMode.BlockMove) { + // Special handling for the double-operand block move. + string opstr1 = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, + gen.Localizer.LabelMap, attr.DataDescriptor, operand >> 8, 1, + PseudoOp.FormatNumericOpFlags.None); + string opstr2 = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, + gen.Localizer.LabelMap, attr.DataDescriptor, operand & 0xff, 1, + PseudoOp.FormatNumericOpFlags.None); + if (gen.Quirks.BlockMoveArgsReversed) { + string tmp = opstr1; + opstr1 = opstr2; + opstr2 = tmp; + } + formattedOperand = opstr1 + "," + opstr2; + } else { + formattedOperand = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, + gen.Localizer.LabelMap, attr.DataDescriptor, + operandForSymbol, operandLen, opFlags); + } + } else { + // Show operand value in hex. + if (op.AddrMode == OpDef.AddressMode.BlockMove) { + int arg1, arg2; + if (gen.Quirks.BlockMoveArgsReversed) { + arg1 = operand & 0xff; + arg2 = operand >> 8; + } else { + arg1 = operand >> 8; + arg2 = operand & 0xff; + } + formattedOperand = formatter.FormatHexValue(arg1, 2) + "," + + formatter.FormatHexValue(arg2, 2); + } else { + if (operandLen == 2) { + // This is necessary for 16-bit operands, like "LDA abs" and "PEA val", + // when outside bank zero. The bank is included in the operand address, + // but we don't want to show it here. + operandForSymbol &= 0xffff; + } + formattedOperand = formatter.FormatHexValue(operandForSymbol, operandLen * 2); + } + } + string operandStr = formatter.FormatOperand(op, formattedOperand, wdis); + + string eolComment = proj.Comments[offset]; + if (doAddCycles) { + bool branchCross = (attr.Address & 0xff00) != (operandForSymbol & 0xff00); + int cycles = proj.CpuDef.GetCycles(op.Opcode, attr.StatusFlags, attr.BranchTaken, + branchCross); + if (cycles > 0) { + eolComment = cycles.ToString() + " " + eolComment; + } else { + eolComment = (-cycles).ToString() + "+ " + eolComment; + } + } + string commentStr = formatter.FormatEolComment(eolComment); + + string replMnemonic = gen.ModifyOpcode(offset, op); + if (attr.Length != instrBytes) { + // This instruction has another instruction inside it. Throw out what we + // computed and just output as bytes. + gen.GenerateShortSequence(offset, instrBytes, out opcodeStr, out operandStr); + } else if (isPcRelBankWrap && gen.Quirks.NoPcRelBankWrap) { + // Some assemblers have trouble generating PC-relative operands that wrap + // around the bank. Output as raw hex. + gen.GenerateShortSequence(offset, instrBytes, out opcodeStr, out operandStr); + } else if (op.AddrMode == OpDef.AddressMode.BlockMove && + gen.Quirks.BlockMoveArgsReversed) { + // On second thought, just don't even output the wrong thing. + gen.GenerateShortSequence(offset, instrBytes, out opcodeStr, out operandStr); + } else if (replMnemonic == null) { + // No mnemonic exists for this opcode. + gen.GenerateShortSequence(offset, instrBytes, out opcodeStr, out operandStr); + } else if (replMnemonic != string.Empty) { + // A replacement mnemonic has been provided. + opcodeStr = formatter.FormatMnemonic(replMnemonic, wdis); + } + gen.OutputLine(labelStr, opcodeStr, operandStr, commentStr); + + // Assemblers like Merlin32 try to be helpful and track SEP/REP, but they do the + // wrong thing if we're in emulation mode. Force flags back to short. + if (proj.CpuDef.HasEmuFlag && gen.Quirks.TracksSepRepNotEmu && op == OpDef.OpREP_Imm) { + if ((operand & 0x30) != 0 && attr.StatusFlags.E == 1) { + gen.OutputRegWidthDirective(offset, 0, 0, 1, 1); + } + } + } + + /// + /// Determines whether the instruction at the specified offset has an operand that is + /// a forward reference. This only matters for single-pass assemblers. + /// + /// Source generator reference. + /// Offset of instruction opcode. + /// True if the instruction's operand is a forward reference to a label. + private static bool IsForwardLabelReference(IGenerator gen, int offset) { + DisasmProject proj = gen.Project; + Debug.Assert(proj.GetAnattrib(offset).IsInstructionStart); + + FormatDescriptor dfd = proj.GetAnattrib(offset).DataDescriptor; + if (dfd == null || !dfd.HasSymbol) { + return false; + } + int labelOffset = proj.FindLabelOffsetByName(dfd.SymbolRef.Label); + if (labelOffset <= offset) { + // Doesn't exist, or is backward reference. + return false; + } + return true; + } + + /// + /// Configures some common format config items from the app settings. Uses a + /// passed-in settings object, rather than the global settings. + /// + /// Application settings. + /// Format config struct. + public static void ConfigureFormatterFromSettings(AppSettings settings, + ref Formatter.FormatConfig config) { + config.mUpperHexDigits = + settings.GetBool(AppSettings.FMT_UPPER_HEX_DIGITS, false); + config.mUpperOpcodes = + settings.GetBool(AppSettings.FMT_UPPER_OP_MNEMONIC, false); + config.mUpperPseudoOpcodes = + settings.GetBool(AppSettings.FMT_UPPER_PSEUDO_OP_MNEMONIC, false); + config.mUpperOperandA = + settings.GetBool(AppSettings.FMT_UPPER_OPERAND_A, false); + config.mUpperOperandS = + settings.GetBool(AppSettings.FMT_UPPER_OPERAND_S, false); + config.mUpperOperandXY = + settings.GetBool(AppSettings.FMT_UPPER_OPERAND_XY, false); + config.mSpacesBetweenBytes = + settings.GetBool(AppSettings.FMT_SPACES_BETWEEN_BYTES, false); + config.mAddSpaceLongComment = + settings.GetBool(AppSettings.FMT_ADD_SPACE_FULL_COMMENT, true); + + config.mForceAbsOpcodeSuffix = + settings.GetString(AppSettings.FMT_OPCODE_SUFFIX_ABS, string.Empty); + config.mForceLongOpcodeSuffix = + settings.GetString(AppSettings.FMT_OPCODE_SUFFIX_LONG, string.Empty); + config.mForceAbsOperandPrefix = + settings.GetString(AppSettings.FMT_OPERAND_PREFIX_ABS, string.Empty); + config.mForceLongOperandPrefix = + settings.GetString(AppSettings.FMT_OPERAND_PREFIX_LONG, string.Empty); + + string exprMode = settings.GetString(AppSettings.FMT_EXPRESSION_MODE, string.Empty); + config.mExpressionMode = Formatter.FormatConfig.ParseExpressionMode(exprMode); + } + } +} diff --git a/SourceGenWPF/AsmGen/IAssembler.cs b/SourceGenWPF/AsmGen/IAssembler.cs new file mode 100644 index 0000000..9d6ec40 --- /dev/null +++ b/SourceGenWPF/AsmGen/IAssembler.cs @@ -0,0 +1,82 @@ +/* + * 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.ComponentModel; + +namespace SourceGenWPF.AsmGen { + /// + /// Common interface for executing assemblers. + /// + public interface IAssembler { + /// + /// Gets identification strings for the executable. These are used when browsing for + /// the assembler binary. + /// + /// Human-readable name to show in the "open" dialog. + /// Name of executable to find, without ".exe". + void GetExeIdentifiers(out string humanName, out string exeName); + + /// + /// Queries the assembler for its default configuration. + /// + /// Config object with default values. + AssemblerConfig GetDefaultConfig(); + + /// + /// Queries the assembler for its version. Assembler executable paths are queried from + /// the global settings object. + /// + /// Assembler version info, or null if query failed. + AssemblerVersion QueryVersion(); + + /// + /// Configures the object. Pass in the list of pathnames returned by IGenerator.Run(), + /// and the working directory to use for the shell command. + /// + /// Assembler source pathnames. + /// Working directory for shell command. + void Configure(List pathNames, string workDirectory); + + /// + /// Executes the assembler. Must call Configure() first. Executed on background thread. + /// + /// Async work object, used to report progress updates and + /// check for cancellation. + /// Execution results, or null on internal failure. + AssemblerResults RunAssembler(BackgroundWorker worker); + } + + /// + /// Set of values returned by the assembler. + /// + public class AssemblerResults { + public string CommandLine { get; private set; } + public int ExitCode { get; private set; } + public string Stdout { get; private set; } + public string Stderr { get; private set; } + public string OutputPathName { get; private set; } + + public AssemblerResults(string commandLine, int exitCode, string stdout, string stderr, + string outputFile) { + CommandLine = commandLine; + ExitCode = exitCode; + Stdout = stdout; + Stderr = stderr; + OutputPathName = outputFile; + } + } +} diff --git a/SourceGenWPF/AsmGen/IGenerator.cs b/SourceGenWPF/AsmGen/IGenerator.cs new file mode 100644 index 0000000..ac207e0 --- /dev/null +++ b/SourceGenWPF/AsmGen/IGenerator.cs @@ -0,0 +1,192 @@ +/* + * 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.ComponentModel; + +using Asm65; + +namespace SourceGenWPF.AsmGen { + /// + /// Common interface for generating assembler-specific source code. + /// + public interface IGenerator { + /// + /// Returns some strings and format options for use in for the display list, configurable + /// through the app settings "quick set" feature. These are not used when generating + /// source code. + /// + /// This may be called on an unconfigured IGenerator. + /// + /// Table of pseudo-op names. + /// Format configuration. + void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps, + out Formatter.FormatConfig formatConfig); + + + /// + /// Configure generator. Must be called before calling any other method or using + /// properties, unless otherwise noted. + /// + /// Project to generate source for. + /// Directory in which to create output files. + /// Name to use as base for filenames. + /// Version of assembler to target. Pass in null + /// to target latest known version. + /// App settings object. + void Configure(DisasmProject project, string workDirectory, string fileNameBase, + AssemblerVersion asmVersion, AppSettings settings); + + /// + /// Project object with file data and Anattribs. + /// + DisasmProject Project { get; } + + /// + /// Source code formatter. + /// + Formatter SourceFormatter { get; } + + /// + /// Application settings. + /// + AppSettings Settings { get; } + + /// + /// Assembler-specific behavior. Used to handle quirky behavior for things that + /// are otherwise managed by common code. + /// + AssemblerQuirks Quirks { get; } + + /// + /// Label localization object. Behavior is assembler-specific. + /// + LabelLocalizer Localizer { get; } + + /// + /// Generates source files on a background thread. Method must not make any UI calls. + /// + /// Async work object, used to report progress updates and + /// check for cancellation. + /// List of pathnames of generated files. + List GenerateSource(BackgroundWorker worker); + + /// + /// Provides an opportunity for the assembler to replace a mnemonic with another, or + /// output an instruction as hex bytes. + /// + /// Opcode offset. + /// Opcode to replace. + /// Replacement mnemonic, an empty string if the original is fine, or + /// null if the op is unsupported or broken and should be emitted as hex. + string ModifyOpcode(int offset, OpDef op); + + /// + /// Generates an opcode/operand pair for a short sequence of bytes (1-4 bytes). + /// Does not produce any source output. + /// + /// Offset to data. + /// Number of bytes (1-4). + /// Opcode mnemonic. + /// Formatted operand. + void GenerateShortSequence(int offset, int length, out string opcode, out string operand); + + /// + /// Outputs zero or more lines of assembler configuration. This comes after the + /// header comment but before any directives. Useful for configuring the CPU type + /// and assembler options. + /// + void OutputAsmConfig(); + + /// + /// Outputs one or more lines of data for the specified offset. + /// + /// Offset to data. + void OutputDataOp(int offset); + + /// + /// Outputs an equate directive. The numeric value is already formatted. + /// + /// Symbol label. + /// Formatted value. + /// End-of-line comment. + void OutputEquDirective(string name, string valueStr, string comment); + + /// + /// Outputs a code origin directive. + /// + /// Offset of code targeted to new address. + /// 24-bit address. + void OutputOrgDirective(int offset, int address); + + /// + /// Notify the assembler of a change in register width. + /// + /// Merlin32 always sets both values (e.g. "MX %00"), cc65 sets each register + /// individually (".A16", ".I8"). We need to accommodate both styles. + /// + /// Offset of change. + /// Previous value for M flag. + /// Previous value for X flag. + /// New value for M flag. + /// New value for X flag. + void OutputRegWidthDirective(int offset, int prevM, int prevX, int newM, int newX); + + /// + /// Output a line of source code. All elements must be fully formatted, except for + /// certain assembler-specific things like ':' on labels. The items will be padded + /// with spaces to fit specific column widths. + /// + /// Optional label. + /// Opcode mnemonic. + /// Operand; may be empty. + /// Optional comment. + void OutputLine(string label, string opcode, string operand, string comment); + + /// + /// Output a line of source code. This will be output as-is. + /// + /// Full text of line to outut. + void OutputLine(string fullLine); + } + + /// + /// Enumeration of quirky or buggy behavior that GenCommon needs to handle. + /// + public class AssemblerQuirks { + /// + /// Are the arguments to MVN/MVP reversed? + /// + public bool BlockMoveArgsReversed { get; set; } + + /// + /// Does the assembler configure assembler widths based on SEP/REP, but doesn't + /// track the emulation bit? + /// + public bool TracksSepRepNotEmu { get; set; } + + /// + /// Is the assembler unable to generate relative branches that wrap around banks? + /// (Note this affects long-distance BRLs that don't appear to wrap.) + /// + public bool NoPcRelBankWrap { get; set; } + + /// + /// Is the assembler implemented as a single pass? + /// + public bool SinglePassAssembler { get; set; } + } +} \ No newline at end of file diff --git a/SourceGenWPF/AsmGen/LabelLocalizer.cs b/SourceGenWPF/AsmGen/LabelLocalizer.cs new file mode 100644 index 0000000..adb29a7 --- /dev/null +++ b/SourceGenWPF/AsmGen/LabelLocalizer.cs @@ -0,0 +1,386 @@ +/* + * 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; +using System.Collections.Generic; +using System.Diagnostics; + +/* +Some assemblers support "local labels", with varying definitions of scope and features. +Generally speaking, local labels only need to be unique within a certain limited scope, and +they aren't included in end-of-assembly symbol lists. + +One popular form defines its scope as being between two global labels. So this is allowed: + + glob1 lda #$00 + :local sta $00 + glob2 lda #$01 + :local sta $01 + +but this would cause an error: + + glob1 lda #$00 + :local sta $00 + glob2 lda #$01 + bne :local + +because the local symbol table is cleared when a global symbol is encountered. + +Another common form allows backward references to labels that don't go out of scope until +they're re-used. This is useful for short loops. + +As a further limitation, assemblers seem to want the first label encountered in a program +to be global. + +The Symbol.SymbolType enum allows a label to be defined as "local or global". We can output +these with the local-symbol syntax, potentially rewriting them to have non-unique names like +"loop", but we can't promote (demote?) a label to local unless there are no references to it +that cross a global label. + +The cross-reference table we generate as part of the analysis process provides a full list of +label references, so we just need to iterate through the label list until we can't find +anything else that needs to be made global. + +Because the definition of "local label" is somewhat assembler-specific, it's best to defer +this analysis to code generation time, when the specific characteristics of the target +assembler can be taken into account. + +References to an offset can be numeric or symbolic. A purely numeric reference like "LDA $2000" +will always map to the offset associated with address $2000, but a symbolic reference might be +offset. For example, the LDA instruction could reference a label at $2008 as "LDA FOO-8". +The assembler cares about the symbolic references, not the actual offsets or addresses. For +this reason we can ignore references to an address with a label if those references don't +actually use the label. (One consequence of this is that formatting an operand as hex +eliminates it from the set of things for us to consider. Also, ORG directives have no effect +on the localizer.) + +Labels that are marked as global, but to which there are no references, could in theory be +elided. To do this we would have to omit them from the generated code, which would be +annoying and weird if (say) the user added them to label an external entry point. + + +The eventual output of our efforts is a map from the original symbol name to the local symbol +name. This must be applied to both labels and operands. +*/ + +namespace SourceGenWPF.AsmGen { + public class LabelLocalizer { + /// + /// A pairing of an offset with a label string. (Essentially mAnattribs[n].Symbol + /// with all the fluff trimmed away.) + /// + /// The label string isn't actually all that useful, since we can pull it back out + /// of anattrib, but it makes life a little easier during debugging. These get + /// put into a List, so switching to a plain int offset doesn't necessarily help us + /// much because the ints get boxed. + /// + private class OffsetLabel { + public int Offset { get; private set; } + public string Label { get; private set; } + + public OffsetLabel(int offset, string label) { + Offset = offset; + Label = label; + } + + public override string ToString() { + return "+" + Offset.ToString("x6") + "(" + Label + ")"; + } + } + + /// + /// A pair of offsets. An operand (instruction or data) at the source offset + /// references a label at the destination offset. + /// + private class OffsetPair { + public int SrcOffset { get; private set; } // offset from which reference is made + public int DstOffset { get; private set; } // offset being referred to + + public OffsetPair(int src, int dst) { + SrcOffset = src; + DstOffset = dst; + } + + public override string ToString() { + return "src=+" + SrcOffset.ToString("x6") + " dst=+" + DstOffset.ToString("x6"); + } + } + + /// + /// Map from label string to local label string. This will be null until Analyze() + /// has executed. + /// + public Dictionary LabelMap { get; private set; } + + /// + /// String to prefix to local labels. Usually a single character, like ':' or '@'. + /// + public string LocalPrefix { get; set; } + + /// + /// Project reference. + /// + private DisasmProject mProject; + + // Work state. + private List mGlobalLabels = new List(); + private List mOffsetPairs = new List(); + private BitArray mGlobalFlags; + + + public LabelLocalizer(DisasmProject project) { + mProject = project; + mGlobalFlags = new BitArray(mProject.FileDataLength); + + LocalPrefix = "!?"; + } + + /// + /// Applies the LabelMap to the label. If the LabelMap is null, or does not have an + /// entry for the label, the original label is returned. + /// + /// Label to convert. + /// New label, or original label. + public string ConvLabel(string label) { + if (LabelMap != null) { + if (LabelMap.TryGetValue(label, out string newLabel)) { + label = newLabel; + } + } + return label; + } + + /// + /// Analyzes labels to identify which ones may be treated as non-global. + /// + public void Analyze() { + Debug.Assert(LocalPrefix.Length > 0); + + mGlobalFlags.SetAll(false); + + // Currently we only support the "local labels have scope that ends at a global + // label" variety. The basic idea is to start by assuming that everything not + // explicitly marked global is local, and then identify situations like this: + // + // lda :local + // global eor #$ff + // :local sta $00 + // + // The reference crosses a global label, so the "target" label must be made global. + // This can have ripple effects, so we have to iterate. Note it doesn't matter + // whether "global" is referenced anywhere. + // + // The current algorithm uses a straightforward O(n^2) approach. + + // Step 1: generate source/target pairs and global label list + GenerateLists(); + + // Step 2: walk through the list of global symbols, identifying source/target + // pairs that cross them. If a pair matches, the target label is added to the + // mGlobalLabels list, and removed from the pair list. + for (int index = 0; index < mGlobalLabels.Count; index++) { + FindIntersectingPairs(mGlobalLabels[index]); + } + + // Step 3: for each local label, add an entry to the map with the appropriate + // local-label syntax. + LabelMap = new Dictionary(); + for (int i = 0; i < mProject.FileDataLength; i++) { + if (mGlobalFlags[i]) { + continue; + } + Symbol sym = mProject.GetAnattrib(i).Symbol; + if (sym == null) { + continue; + } + + LabelMap[sym.Label] = LocalPrefix + sym.Label; + } + + // Take out the trash. + mGlobalLabels.Clear(); + mOffsetPairs.Clear(); + } + + /// + /// Generates the initial mGlobalFlags and mGlobalLabels lists, as well as the + /// full cross-reference pair list. + /// + private void GenerateLists() { + // For every offset that has a label, add an entry to the source/target pair list + // for every offset that references it. + // + // If the label isn't marked as "local or global", add it to the global-label list. + // + // The first label encountered is always treated as global. Note it may not appear + // at offset zero. + + bool first = true; + + for (int i = 0; i < mProject.FileDataLength; i++) { + Symbol sym = mProject.GetAnattrib(i).Symbol; + if (sym == null) { + // No label at this offset. + continue; + } + + if (first || sym.SymbolType != Symbol.Type.LocalOrGlobalAddr) { + first = false; + mGlobalFlags[i] = true; + mGlobalLabels.Add(new OffsetLabel(i, sym.Label)); + + // Don't add to pairs list. + continue; + } + + // If nothing actually references this label, the xref set will be empty. + XrefSet xrefs = mProject.GetXrefSet(i); + if (xrefs != null) { + foreach (XrefSet.Xref xref in xrefs) { + if (!xref.IsSymbolic) { + continue; + } + + mOffsetPairs.Add(new OffsetPair(xref.Offset, i)); + } + } + } + } + + /// + /// Identifies all label reference pairs that cross the specified global label. When + /// a matching pair is found, the pair's destination label is marked as global and + /// added to the global label list. + /// + /// Global label of interest. + private void FindIntersectingPairs(OffsetLabel glabel) { + Debug.Assert(mGlobalFlags[glabel.Offset]); + + int globOffset = glabel.Offset; + for (int i = 0; i < mOffsetPairs.Count; i++) { + OffsetPair pair = mOffsetPairs[i]; + + // If the destination was marked global earlier, remove and ignore this entry. + // Note this also means that pair.DstOffset != label.Offset. + if (mGlobalFlags[pair.DstOffset]) { + mOffsetPairs.RemoveAt(i); + i--; + continue; + } + + // Check to see if the global label falls between the source and destination + // offsets. + // + // If the reference source is itself a global label, it can reference local + // labels forward, but not backward. We need to take that into account for + // the case where label.Offset==pair.SrcOffset. + bool intersect; + if (pair.SrcOffset < pair.DstOffset) { + // Forward reference. src==glob is ok + intersect = pair.SrcOffset < globOffset && pair.DstOffset >= globOffset; + } else { + // Backward reference. src==glob is bad + intersect = pair.SrcOffset >= globOffset && pair.DstOffset <= globOffset; + } + + if (intersect) { + //Debug.WriteLine("Global " + glabel + " btwn " + pair + " (" + + // mProject.GetAnattrib(pair.DstOffset).Symbol.Label + ")"); + + // Change the destination label to global. + mGlobalFlags[pair.DstOffset] = true; + mGlobalLabels.Add(new OffsetLabel(pair.DstOffset, + mProject.GetAnattrib(pair.DstOffset).Symbol.Label)); + + // Carefully remove it from the list we're iterating through. + mOffsetPairs.RemoveAt(i); + i--; + } + } + } + + /// + /// Adjusts the label map so that only local variables start with an underscore ('_'). + /// This is necessary for assemblers like 64tass that use a leading underscore to + /// indicate that a label should be local. + /// + /// This may be called even if label localization is disabled. In that case we just + /// create an empty label map and populate as needed. + /// + /// Only call this if underscores are used to indicate local labels. + /// + public void MaskLeadingUnderscores() { + bool allGlobal = false; + if (LabelMap == null) { + allGlobal = true; + LabelMap = new Dictionary(); + } + + // Throw out the original local label generation. + LabelMap.Clear(); + + // Use this to test for uniqueness. We add all labels here as we go, not just the + // ones being remapped. For each label we either add the original or the localized + // form. + SortedList allLabels = new SortedList(); + + for (int i = 0; i < mProject.FileDataLength; i++) { + Symbol sym = mProject.GetAnattrib(i).Symbol; + if (sym == null) { + // No label at this offset. + continue; + } + + string newLabel; + if (allGlobal || mGlobalFlags[i]) { + // Global symbol. Don't let it start with '_'. + if (sym.Label.StartsWith("_")) { + // There's an underscore here that was added by the user. Stick some + // other character in front. + newLabel = "X" + sym.Label; + } else { + // No change needed. + newLabel = sym.Label; + } + } else { + // Local symbol. + if (sym.Label.StartsWith("_")) { + // The original starts with one or more underscores. Adding another + // will create a "__" label, which is reserved in 64tass. + newLabel = "_X" + sym.Label; + } else { + newLabel = "_" + sym.Label; + } + } + + // Make sure it's unique. + string uniqueLabel = newLabel; + int uval = 1; + while (allLabels.ContainsKey(uniqueLabel)) { + uniqueLabel = newLabel + uval.ToString(); + } + allLabels.Add(uniqueLabel, uniqueLabel); + + // If it's different, add it to the label map. + if (sym.Label != uniqueLabel) { + LabelMap.Add(sym.Label, uniqueLabel); + } + } + + Debug.WriteLine("UMAP: allcount=" + allLabels.Count + " mapcount=" + LabelMap.Count); + } + } +} diff --git a/SourceGenWPF/AsmGen/StringGather.cs b/SourceGenWPF/AsmGen/StringGather.cs new file mode 100644 index 0000000..cb3498f --- /dev/null +++ b/SourceGenWPF/AsmGen/StringGather.cs @@ -0,0 +1,227 @@ +/* + * 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.Diagnostics; + +namespace SourceGenWPF.AsmGen { + /// + /// Multi-line string gatherer. Accumulates characters and raw bytes, emitting + /// them when we have a full operand's worth. + /// + /// If the delimiter character appears, it will be output inline as a raw byte. + /// The low-ASCII string ['hello'world'] will become [27,'hello',27,'world',27] + /// (or something similar). + /// + public class StringGather { + // Inputs. + public IGenerator Gen { get; private set; } + public string Label { get; private set; } + public string Opcode { get; private set; } + public string Comment { get; private set; } + public char Delimiter { get; private set; } + public char DelimiterReplacement { get; private set; } + public ByteStyle ByteStyleX { get; private set; } + public int MaxOperandLen { get; private set; } + public bool IsTestRun { get; private set; } + + public enum ByteStyle { DenseHex, CommaSep }; + + // Outputs. + public bool HasDelimiter { get; private set; } + public int NumLinesOutput { get; private set; } + + private char[] mHexChars; + + /// + /// Character collection buffer. The delimiters are written into the buffer + /// because they're mixed with bytes, particularly when we have to escape the + /// delimiter character. Strings might start or end with escaped delimiters, + /// so we don't add them until we have to. + private char[] mBuffer; + + /// + /// Next available character position. + /// + private int mIndex = 0; + + /// + /// State of the buffer, based on the last thing we added. + /// + private enum State { + Unknown = 0, + StartOfLine, + InQuote, + OutQuote + } + private State mState = State.StartOfLine; + + /// + /// Constructor. + /// + /// Reference back to generator, for output function and + /// format options. + /// Line label. Appears on first output line only. + /// Opcode to use for all lines. + /// End-of-line comment. Appears on first output line + /// only. + /// String delimiter character. + /// If true, no file output is produced. + public StringGather(IGenerator gen, string label, string opcode, + string comment, char delimiter, char delimReplace, ByteStyle byteStyle, + int maxOperandLen, bool isTestRun) { + Gen = gen; + Label = label; + Opcode = opcode; + Comment = comment; + Delimiter = delimiter; + DelimiterReplacement = delimReplace; + ByteStyleX = byteStyle; + MaxOperandLen = maxOperandLen; + IsTestRun = isTestRun; + + mBuffer = new char[MaxOperandLen]; + mHexChars = Gen.SourceFormatter.HexDigits; + } + + /// + /// Write a character into the buffer. + /// + /// Character to add. + public void WriteChar(char ch) { + Debug.Assert(ch >= 0 && ch <= 0xff); + if (ch == Delimiter) { + // Must write it as a byte. + HasDelimiter = true; + WriteByte((byte)DelimiterReplacement); + return; + } + + // If we're at the start of a line, add delimiter, then new char. + // If we're inside quotes, just add the character. We must have space for + // two chars (new char, close quote). + // If we're outside quotes, add a comma and delimiter, then the character. + // We must have 4 chars remaining (comma, open quote, new char, close quote). + switch (mState) { + case State.StartOfLine: + mBuffer[mIndex++] = Delimiter; + break; + case State.InQuote: + if (mIndex + 2 > MaxOperandLen) { + Flush(); + mBuffer[mIndex++] = Delimiter; + } + break; + case State.OutQuote: + if (mIndex + 4 > MaxOperandLen) { + Flush(); + mBuffer[mIndex++] = Delimiter; + } else { + mBuffer[mIndex++] = ','; + mBuffer[mIndex++] = Delimiter; + } + break; + default: + Debug.Assert(false); + break; + } + mBuffer[mIndex++] = ch; + mState = State.InQuote; + } + + /// + /// Write a hex value into the buffer. + /// + /// Value to add. + public void WriteByte(byte val) { + // If we're at the start of a line, just output the byte. + // If we're inside quotes, emit a delimiter, comma, and the byte. We must + // have space for four (DenseHex) or five (CommaSep) chars. + // If we're outside quotes, add the byte. We must have two (DenseHex) or + // four (CommaSep) chars remaining. + switch (mState) { + case State.StartOfLine: + break; + case State.InQuote: + int minWidth = (ByteStyleX == ByteStyle.CommaSep) ? 5 : 4; + if (mIndex + minWidth > MaxOperandLen) { + Flush(); + } else { + mBuffer[mIndex++] = Delimiter; + mBuffer[mIndex++] = ','; + } + break; + case State.OutQuote: + minWidth = (ByteStyleX == ByteStyle.CommaSep) ? 4 : 2; + if (mIndex + minWidth > MaxOperandLen) { + Flush(); + } else { + if (ByteStyleX == ByteStyle.CommaSep) { + mBuffer[mIndex++] = ','; + } + } + break; + default: + Debug.Assert(false); + break; + } + + if (ByteStyleX == ByteStyle.CommaSep) { + mBuffer[mIndex++] = '$'; + } + mBuffer[mIndex++] = mHexChars[val >> 4]; + mBuffer[mIndex++] = mHexChars[val & 0x0f]; + mState = State.OutQuote; + } + + /// + /// Tells the object to flush any pending data to the output. + /// + public void Finish() { + Flush(); + } + + /// + /// Outputs the buffer of pending data. A closing delimiter will be added if needed. + /// + private void Flush() { + switch (mState) { + case State.StartOfLine: + // empty string; put out a pair of delimiters + mBuffer[mIndex++] = Delimiter; + mBuffer[mIndex++] = Delimiter; + NumLinesOutput++; + break; + case State.InQuote: + // add delimiter and finish + mBuffer[mIndex++] = Delimiter; + NumLinesOutput++; + break; + case State.OutQuote: + // just output it + NumLinesOutput++; + break; + } + if (!IsTestRun) { + Gen.OutputLine(Label, Opcode, new string(mBuffer, 0, mIndex), + Comment); + } + mIndex = 0; + + // Erase these after first use so we don't put them on every line. + Label = Comment = string.Empty; + } + } +} diff --git a/SourceGenWPF/DisplayList.cs b/SourceGenWPF/DisplayList.cs index 00f0481..c646bb2 100644 --- a/SourceGenWPF/DisplayList.cs +++ b/SourceGenWPF/DisplayList.cs @@ -35,11 +35,15 @@ namespace SourceGenWPF { /// /// The ItemsControl.ItemsSource property wants an IEnumerable (which IList implements). /// According to various articles, if the object implements IList, and the UI element - /// is providing UI virtualization, you will also get data virtualization. This behavior + /// is providing *UI* virtualization, you will also get *data* virtualization. This behavior /// doesn't seem to be documented anywhere, but the consensus is that it's expected to work. /// - /// Implementing generic IList doesn't seem necessary for XAML, but is useful for other - /// customers of the data (e.g. the assembler source generator). + /// Implementing generic IList<> doesn't seem necessary for XAML, but may be useful + /// for other consumers of the data. + /// + /// The list is initially filled with null references, with FormattedParts instances + /// generated on demand. This is done by requesting individual items from the + /// DisplayListGen object. /// public class DisplayList : IList, IList, INotifyCollectionChanged, INotifyPropertyChanged { @@ -47,11 +51,20 @@ namespace SourceGenWPF { // TODO: check VirtualizingStackPanel.VirtualizationMode == recycling (page 259) /// - /// List of formatted parts. The idea is that the list is initially populated with - /// null references, and FormattedParts objects are generated on demand. + /// List of formatted parts. DO NOT access this directly outside the event-sending + /// method wrappers. /// private List mList; + /// + /// Data generation object. + /// + /// + /// This property is set by the LineListGen constructor. + /// + public LineListGen ListGen { get; set; } + + /// /// Constructs an empty collection, with the default initial capacity. /// @@ -59,14 +72,6 @@ namespace SourceGenWPF { mList = new List(); } - public DisplayList(int count) { - mList = new List(count); - for (int i = 0; i < count; i++) { - mList.Add(null); - } - } - - #region Property / Collection Changed @@ -279,12 +284,18 @@ namespace SourceGenWPF { /// Retrieves the Nth element. /// private FormattedParts GetEntry(int index) { - Debug.WriteLine("GEN " + index); - if ((index % 10) != 0) { - return FormattedParts.Create("off" + index, "addr" + index, "12 34", - "vncidmx", "", "yup:", "LDA", "$1234", "a & b"); - } else { - return FormattedParts.Create("yup: This is a long comment line"); + FormattedParts parts = mList[index]; + if (parts == null) { + parts = mList[index] = ListGen.GetFormattedParts(index); + } + return parts; + } + + public void ResetList(int size) { + Clear(); + mList.Capacity = size; + for (int i = 0; i < size; i++) { + Add(null); } } @@ -328,6 +339,33 @@ namespace SourceGenWPF { return parts; } + public static FormattedParts CreateBlankLine() { + FormattedParts parts = new FormattedParts(); + return parts; + } + + public static FormattedParts CreateLongComment(string comment) { + FormattedParts parts = new FormattedParts(); + parts.Comment = comment; + return parts; + } + + public static FormattedParts CreateDirective(string opstr, string addrStr) { + FormattedParts parts = new FormattedParts(); + parts.Opcode = opstr; + parts.Operand = addrStr; + return parts; + } + + public static FormattedParts CreateEquDirective(string label, string opstr, + string addrStr, string comment) { + FormattedParts parts = new FormattedParts(); + parts.Label = label; + parts.Opcode = opstr; + parts.Operand = addrStr; + parts.Comment = comment; + return parts; + } } } } diff --git a/SourceGenWPF/DisplayListGen.cs b/SourceGenWPF/LineListGen.cs similarity index 94% rename from SourceGenWPF/DisplayListGen.cs rename to SourceGenWPF/LineListGen.cs index ee46d02..89edb79 100644 --- a/SourceGenWPF/DisplayListGen.cs +++ b/SourceGenWPF/LineListGen.cs @@ -20,17 +20,28 @@ using System.Windows.Media; using System.Text; using Asm65; +using FormattedParts = SourceGenWPF.DisplayList.FormattedParts; namespace SourceGenWPF { /// /// Converts file data and Anattrib contents into a series of strings and format metadata. /// - public class DisplayListGen { + public class LineListGen { /// /// List of display lines. /// private List mLineList; + /// + /// List of formatted parts to be presented to the user. This has one entry per line. + /// + /// + /// Separating FormattedParts out of Line seems odd at first, but we need changes to + /// DisplayList to cause events in XAML. I'm thinking the artificial separation of + /// Line from the formatted data holder may make future ports easier. + /// + private DisplayList mDisplayList; + /// /// Project that contains the data we're formatting, notably the FileData and /// Anattribs arrays. @@ -53,68 +64,6 @@ namespace SourceGenWPF { private PseudoOp.PseudoOpNames mPseudoOpNames; - /// - /// Holds a collection of formatted strings. Instances are immutable. - /// - public class FormattedParts { - public string Offset { get; private set; } - public string Addr { get; private set; } - public string Bytes { get; private set; } - public string Flags { get; private set; } - public string Attr { get; private set; } - public string Label { get; private set; } - public string Opcode { get; private set; } - public string Operand { get; private set; } - public string Comment { get; private set; } - - // Use factory methods. - private FormattedParts() { } - - public static FormattedParts Create(string offset, string addr, string bytes, - string flags, string attr, string label, string opcode, string operand, - string comment, string debug) { - FormattedParts parts = new FormattedParts(); - parts.Offset = offset; - parts.Addr = addr; - parts.Bytes = bytes; - parts.Flags = flags; - parts.Attr = attr; - parts.Label = label; - parts.Opcode = opcode; - parts.Operand = operand; - parts.Comment = comment; - return parts; - } - - public static FormattedParts CreateBlankLine() { - FormattedParts parts = new FormattedParts(); - return parts; - } - - public static FormattedParts CreateLongComment(string comment) { - FormattedParts parts = new FormattedParts(); - parts.Comment = comment; - return parts; - } - - public static FormattedParts CreateDirective(string opstr, string addrStr) { - FormattedParts parts = new FormattedParts(); - parts.Opcode = opstr; - parts.Operand = addrStr; - return parts; - } - - public static FormattedParts CreateEquDirective(string label, string opstr, - string addrStr, string comment) { - FormattedParts parts = new FormattedParts(); - parts.Label = label; - parts.Opcode = opstr; - parts.Operand = addrStr; - parts.Comment = comment; - return parts; - } - } - /// /// One of these per line of output in the display. It should be possible to draw /// all of the output without needing to refer back to the project data. (Currently @@ -174,6 +123,11 @@ namespace SourceGenWPF { /// Strings for display. Creation may be deferred. Use the DisplayList /// GetFormattedParts() method to access this property. /// + /// + /// Certain elements, such as multi-line comments, must be formatted to determine + /// the number of lines they span. We retain the results to avoid formatting + /// them twice. + /// public FormattedParts Parts { get; set; } /// @@ -277,7 +231,7 @@ namespace SourceGenWPF { /// Display list, with list of Lines. /// Bit vector specifying which lines are selected. /// New SavedSelection object. - public static SavedSelection Generate(DisplayListGen dl, VirtualListViewSelection sel, + public static SavedSelection Generate(LineListGen dl, VirtualListViewSelection sel, int topOffset) { SavedSelection savedSel = new SavedSelection(); //Debug.Assert(topOffset >= 0); @@ -343,7 +297,7 @@ namespace SourceGenWPF { /// /// Display list, with list of Lines. /// Set of selected lines. - public VirtualListViewSelection Restore(DisplayListGen dl, out int topIndex) { + public VirtualListViewSelection Restore(LineListGen dl, out int topIndex) { List lineList = dl.mLineList; VirtualListViewSelection sel = new VirtualListViewSelection(lineList.Count); @@ -414,15 +368,23 @@ namespace SourceGenWPF { /// /// Project object. /// Formatter object. - public DisplayListGen(DisasmProject proj, Formatter formatter, + public LineListGen(DisasmProject proj, DisplayList displayList, Formatter formatter, PseudoOp.PseudoOpNames opNames) { + Debug.Assert(proj != null); + Debug.Assert(displayList != null); + Debug.Assert(formatter != null); + Debug.Assert(opNames != null); + mProject = proj; + mDisplayList = displayList; mFormatter = formatter; mPseudoOpNames = opNames; mLineList = new List(); mShowCycleCounts = AppSettings.Global.GetBool(AppSettings.SRCGEN_SHOW_CYCLE_COUNTS, false); + + mDisplayList.ListGen = this; } /// @@ -432,6 +394,7 @@ namespace SourceGenWPF { public void SetFormatter(Formatter formatter) { mFormatter = formatter; mLineList.Clear(); + // TODO: update display list // We probably just changed settings, so update this as well. mShowCycleCounts = AppSettings.Global.GetBool(AppSettings.SRCGEN_SHOW_CYCLE_COUNTS, @@ -446,6 +409,7 @@ namespace SourceGenWPF { public void SetPseudoOpNames(PseudoOp.PseudoOpNames opNames) { mPseudoOpNames = opNames; mLineList.Clear(); + // TODO: update display list } /// @@ -490,8 +454,7 @@ namespace SourceGenWPF { // should have been done already default: Debug.Assert(false); - parts = FormattedParts.Create("x", "x", "x", "x", "x", "x", "x", "x", - "x", "x"); + parts = FormattedParts.Create("x", "x", "x", "x", "x", "x", "x", "x", "x"); break; } line.Parts = parts; @@ -617,6 +580,8 @@ namespace SourceGenWPF { GenerateLineList(mProject, mFormatter, mPseudoOpNames, 0, mProject.FileData.Length - 1, mLineList); + mDisplayList.ResetList(mLineList.Count); + Debug.Assert(ValidateLineList(), "Display list failed validation"); } @@ -689,6 +654,7 @@ namespace SourceGenWPF { // Out with the old, in with the new. mLineList.RemoveRange(startIndex, endIndex - startIndex + 1); mLineList.InsertRange(startIndex, newLines); + // TODO: update display list Debug.Assert(ValidateLineList(), "Display list failed validation"); } @@ -754,6 +720,7 @@ namespace SourceGenWPF { } Debug.WriteLine("Removing " + endIndex + " header lines"); mLineList.RemoveRange(0, endIndex); + // TODO: update display list } /// @@ -1169,12 +1136,8 @@ namespace SourceGenWPF { } string commentStr = formatter.FormatEolComment(eolComment); - string debugStr = string.Empty; - //debugStr = "opOff=" + - // (attr.OperandOffset < 0 ? "-" : "+" + attr.OperandOffset.ToString("x6")); - FormattedParts parts = FormattedParts.Create(offsetStr, addrStr, bytesStr, - flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr, debugStr); + flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr); return parts; } @@ -1184,9 +1147,9 @@ namespace SourceGenWPF { byte[] data = proj.FileData; string offsetStr, addrStr, bytesStr, flagsStr, attrStr, labelStr, opcodeStr, - operandStr, commentStr, debugStr; + operandStr, commentStr; offsetStr = addrStr = bytesStr = flagsStr = attrStr = labelStr = opcodeStr = - operandStr = commentStr = debugStr = string.Empty; + operandStr = commentStr = string.Empty; PseudoOp.PseudoOut pout = PseudoOp.FormatDataOp(formatter, opNames, proj.SymbolTable, null, attr.DataDescriptor, proj.FileData, offset, subLineIndex); @@ -1210,13 +1173,10 @@ namespace SourceGenWPF { if (subLineIndex == 0) { commentStr = formatter.FormatEolComment(proj.Comments[offset]); - - //debugStr = "opOff=" + - // (attr.OperandOffset < 0 ? "-" : "+" + attr.OperandOffset.ToString("x6")); } FormattedParts parts = FormattedParts.Create(offsetStr, addrStr, bytesStr, - flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr, debugStr); + flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr); return parts; } } diff --git a/SourceGenWPF/MainController.cs b/SourceGenWPF/MainController.cs index b57edd4..0146750 100644 --- a/SourceGenWPF/MainController.cs +++ b/SourceGenWPF/MainController.cs @@ -51,7 +51,6 @@ namespace SourceGenWPF { /// an empty symbol table. /// private SymbolTableSubset mSymbolSubset; -#endif /// /// Current code list view selection. The length will match the DisplayList Count. @@ -61,11 +60,12 @@ namespace SourceGenWPF { /// notifies us of changes to the selection, so we can track it ourselves. /// private VirtualListViewSelection mCodeViewSelection = new VirtualListViewSelection(); +#endif /// - /// Data backing the codeListView. + /// Data backing the code list. /// - private DisplayListGen mDisplayList; + public LineListGen CodeListGen { get; private set; } #endregion Project state @@ -151,6 +151,171 @@ namespace SourceGenWPF { mMainWin = win; } + /// + /// Perform one-time initialization after the Window has finished loading. We defer + /// to this point so we can report fatal errors directly to the user. + /// + public void WindowLoaded() { + if (RuntimeDataAccess.GetDirectory() == null) { + MessageBox.Show(Res.Strings.RUNTIME_DIR_NOT_FOUND, + Res.Strings.RUNTIME_DIR_NOT_FOUND_CAPTION, + MessageBoxButton.OK, MessageBoxImage.Error); + Application.Current.Shutdown(); + return; + } +#if false + try { + PluginDllCache.PreparePluginDir(); + } catch (Exception ex) { + string pluginPath = PluginDllCache.GetPluginDirPath(); + if (pluginPath == null) { + pluginPath = ""; + } + string msg = string.Format(Properties.Resources.PLUGIN_DIR_FAIL, + pluginPath + ": " + ex.Message); + MessageBox.Show(this, msg, Properties.Resources.PLUGIN_DIR_FAIL_CAPTION, + MessageBoxButtons.OK, MessageBoxIcon.Error); + Application.Exit(); + return; + } +#endif + +#if false + logoPictureBox.ImageLocation = RuntimeDataAccess.GetPathName(LOGO_FILE_NAME); + versionLabel.Text = string.Format(Properties.Resources.VERSION_FMT, + Program.ProgramVersion); + + toolStripStatusLabel.Text = Properties.Resources.STATUS_READY; + + mProjectControl = this.codeListView; + mNoProjectControl = this.noProjectPanel; + + // Clone the menu structure from the designer. The same items are used for + // both Edit > Actions and the right-click context menu in codeListView. + mActionsMenuItems = new ToolStripItem[actionsToolStripMenuItem.DropDownItems.Count]; + for (int i = 0; i < actionsToolStripMenuItem.DropDownItems.Count; i++) { + mActionsMenuItems[i] = actionsToolStripMenuItem.DropDownItems[i]; + } +#endif + +#if false + // Load the settings from the file. Some things (like the symbol subset) need + // these. The general "apply settings" doesn't happen until a bit later, after + // the sub-windows have been initialized. + LoadAppSettings(); + + // Init primary ListView (virtual, ownerdraw) + InitCodeListView(); + + // Init Symbols ListView (virtual, non-ownerdraw) + mSymbolSubset = new SymbolTableSubset(new SymbolTable()); + symbolListView.SetDoubleBuffered(true); + InitSymbolListView(); + + // Init References ListView (non-virtual, non-ownerdraw) + referencesListView.SetDoubleBuffered(true); + + // Place the main window and apply the various settings. + SetAppWindowLocation(); +#endif + ApplyAppSettings(); + +#if false + UpdateActionMenu(); + UpdateMenuItemsAndTitle(); + UpdateRecentLinks(); + + ShowNoProject(); +#endif + + ProcessCommandLine(); + } + + private void ProcessCommandLine() { + string[] args = Environment.GetCommandLineArgs(); + if (args.Length == 2) { + DoOpenFile(Path.GetFullPath(args[1])); + } + } + + /// + /// Applies "actionable" settings to the ProjectView, pulling them out of the global + /// settings object. If a project is open, refreshes the display list and all sub-windows. + /// + private void ApplyAppSettings() { + Debug.WriteLine("ApplyAppSettings..."); + AppSettings settings = AppSettings.Global; + + // Set up the formatter. + mFormatterConfig = new Formatter.FormatConfig(); + AsmGen.GenCommon.ConfigureFormatterFromSettings(AppSettings.Global, + ref mFormatterConfig); + mFormatterConfig.mEndOfLineCommentDelimiter = ";"; + mFormatterConfig.mFullLineCommentDelimiterBase = ";"; + mFormatterConfig.mBoxLineCommentDelimiter = string.Empty; + mFormatterConfig.mAllowHighAsciiCharConst = true; + mOutputFormatter = new Formatter(mFormatterConfig); + mOutputFormatterCpuDef = null; + + // Set pseudo-op names. Entries aren't allowed to be blank, so we start with the + // default values and merge in whatever the user has configured. + mPseudoOpNames = PseudoOp.sDefaultPseudoOpNames.GetCopy(); + string pseudoCereal = settings.GetString(AppSettings.FMT_PSEUDO_OP_NAMES, null); + if (!string.IsNullOrEmpty(pseudoCereal)) { + PseudoOp.PseudoOpNames deser = PseudoOp.PseudoOpNames.Deserialize(pseudoCereal); + if (deser != null) { + mPseudoOpNames.Merge(deser); + } + } + +#if false + // Configure the Symbols window. + symbolUserCheckBox.Checked = + settings.GetBool(AppSettings.SYMWIN_SHOW_USER, false); + symbolAutoCheckBox.Checked = + settings.GetBool(AppSettings.SYMWIN_SHOW_AUTO, false); + symbolProjectCheckBox.Checked = + settings.GetBool(AppSettings.SYMWIN_SHOW_PROJECT, false); + symbolPlatformCheckBox.Checked = + settings.GetBool(AppSettings.SYMWIN_SHOW_PLATFORM, false); + symbolConstantCheckBox.Checked = + settings.GetBool(AppSettings.SYMWIN_SHOW_CONST, false); + symbolAddressCheckBox.Checked = + settings.GetBool(AppSettings.SYMWIN_SHOW_ADDR, false); + + // Set the code list view font. + string fontStr = settings.GetString(AppSettings.CDLV_FONT, null); + if (!string.IsNullOrEmpty(fontStr)) { + FontConverter cvt = new FontConverter(); + try { + Font font = cvt.ConvertFromInvariantString(fontStr) as Font; + codeListView.Font = font; + Debug.WriteLine("Set font to " + font.ToString()); + } catch (Exception ex) { + Debug.WriteLine("Font convert failed: " + ex.Message); + } + } + + // Unpack the recent-project list. + UnpackRecentProjectList(); + + // Enable the DEBUG menu if configured. + bool showDebugMenu = AppSettings.Global.GetBool(AppSettings.DEBUG_MENU_ENABLED, false); + if (dEBUGToolStripMenuItem.Visible != showDebugMenu) { + dEBUGToolStripMenuItem.Visible = showDebugMenu; + mainMenuStrip.Refresh(); + } +#endif + + // Finally, update the display list generator with all the fancy settings. + if (CodeListGen != null) { + // Regenerate the display list with the latest formatter config and + // pseudo-op definition. (These are set as part of the refresh.) + UndoableChange uc = + UndoableChange.CreateDummyChange(UndoableChange.ReanalysisScope.DisplayOnly); + ApplyChanges(new ChangeSet(uc), false); + } + } /// /// Ensures that the named project is at the top of the list. If it's elsewhere @@ -519,7 +684,7 @@ namespace SourceGenWPF { proj.Initialize(fileData.Length); proj.PrepForNew(fileData, Path.GetFileName(dataPathName)); - proj.LongComments.Add(DisplayListGen.Line.HEADER_COMMENT_OFFSET, + proj.LongComments.Add(LineListGen.Line.HEADER_COMMENT_OFFSET, new MultiLineComment("6502bench SourceGen v" + App.ProgramVersion)); // The system definition provides a set of defaults that can be overridden. @@ -540,7 +705,8 @@ namespace SourceGenWPF { dlg.ShowDialog(); } - mDisplayList = new DisplayListGen(mProject, mOutputFormatter, mPseudoOpNames); + CodeListGen = new LineListGen(mProject, mMainWin.CodeDisplayList, + mOutputFormatter, mPseudoOpNames); // Prep the symbol table subset object. Replace the old one with a new one. //mSymbolSubset = new SymbolTableSubset(mProject.SymbolTable); @@ -624,9 +790,9 @@ namespace SourceGenWPF { #else int topItem = 0; #endif - int topOffset = mDisplayList[topItem].FileOffset; - DisplayListGen.SavedSelection savedSel = DisplayListGen.SavedSelection.Generate( - mDisplayList, mCodeViewSelection, topOffset); + int topOffset = CodeListGen[topItem].FileOffset; + LineListGen.SavedSelection savedSel = LineListGen.SavedSelection.Generate( + CodeListGen, null /*mCodeViewSelection*/, topOffset); //savedSel.DebugDump(); mReanalysisTimer.EndTask("Save selection"); @@ -647,7 +813,7 @@ namespace SourceGenWPF { } mReanalysisTimer.EndTask(refreshTaskStr); - VirtualListViewSelection newSel = savedSel.Restore(mDisplayList, out int topIndex); + VirtualListViewSelection newSel = savedSel.Restore(CodeListGen, out int topIndex); //newSel.DebugDump(); // Refresh the various windows, and restore the selection. @@ -708,8 +874,8 @@ namespace SourceGenWPF { Debug.WriteLine("CpuDef has changed, resetting formatter (now " + mProject.CpuDef + ")"); mOutputFormatter = new Formatter(mFormatterConfig); - mDisplayList.SetFormatter(mOutputFormatter); - mDisplayList.SetPseudoOpNames(mPseudoOpNames); + CodeListGen.SetFormatter(mOutputFormatter); + CodeListGen.SetPseudoOpNames(mPseudoOpNames); mOutputFormatterCpuDef = mProject.CpuDef; } @@ -733,8 +899,10 @@ namespace SourceGenWPF { toolStripStatusLabel.Text = prevStatus; } } else { +#endif DoRefreshProject(reanalysisRequired); - } +#if false + } #endif if (FormatDescriptor.DebugCreateCount != 0) { @@ -755,7 +923,7 @@ namespace SourceGenWPF { IEnumerator iter = offsetSet.RangeListIterator; while (iter.MoveNext()) { RangeSet.Range range = iter.Current; - mDisplayList.GenerateRange(range.Low, range.High); + CodeListGen.GenerateRange(range.Low, range.High); } } @@ -783,7 +951,7 @@ namespace SourceGenWPF { } mReanalysisTimer.StartTask("Generate DisplayList"); - mDisplayList.GenerateAll(); + CodeListGen.GenerateAll(); mReanalysisTimer.EndTask("Generate DisplayList"); } diff --git a/SourceGenWPF/ProjWin/MainWindow.xaml b/SourceGenWPF/ProjWin/MainWindow.xaml index 7fe5e13..c8584a7 100644 --- a/SourceGenWPF/ProjWin/MainWindow.xaml +++ b/SourceGenWPF/ProjWin/MainWindow.xaml @@ -22,7 +22,8 @@ limitations under the License. mc:Ignorable="d" Title="6502bench SourceGen" Icon="/SourceGenWPF;component/Res/SourceGenIcon.ico" - Width="810" Height="510" MinWidth="800" MinHeight="500"> + Width="810" Height="510" MinWidth="800" MinHeight="500" + Loaded="Window_Loaded"> @@ -31,13 +32,6 @@ limitations under the License. - - - Ctrl+Shift+A - - - - - + @@ -155,7 +156,7 @@ limitations under the License. - + @@ -163,7 +164,7 @@ limitations under the License. - @@ -174,7 +175,7 @@ limitations under the License. - @@ -245,7 +246,7 @@ limitations under the License. - + diff --git a/SourceGenWPF/ProjWin/MainWindow.xaml.cs b/SourceGenWPF/ProjWin/MainWindow.xaml.cs index 487758e..85e8da9 100644 --- a/SourceGenWPF/ProjWin/MainWindow.xaml.cs +++ b/SourceGenWPF/ProjWin/MainWindow.xaml.cs @@ -36,23 +36,41 @@ namespace SourceGenWPF.ProjWin { /// public partial class MainWindow : Window, INotifyPropertyChanged { /// - private MainController mUI; + /// Disassembled code display list provided to XAML. + /// + public DisplayList CodeDisplayList { get; private set; } + + /// + /// + private MainController mMainCtrl; + public MainWindow() { InitializeComponent(); - // TODO: verify that RuntimeData dir is accessible - this.DataContext = this; - mUI = new MainController(this); - codeListView.ItemsSource = new DisplayList(500); + CodeDisplayList = new DisplayList(); + codeListView.ItemsSource = CodeDisplayList; - GridView gv = (GridView)codeListView.View; + mMainCtrl = new MainController(this); + + //GridView gv = (GridView)codeListView.View; //gv.Columns[0].Width = 50; } + private void Window_Loaded(object sender, RoutedEventArgs e) { + mMainCtrl.WindowLoaded(); + +#if DEBUG + // Get more info on CollectionChanged events that do not agree with current + // state of Items collection. + PresentationTraceSources.SetTraceLevel(codeListView.ItemContainerGenerator, + PresentationTraceLevel.High); + } +#endif + /// /// INotifyPropertyChanged event /// @@ -119,7 +137,7 @@ namespace SourceGenWPF.ProjWin { recentIndex--; Debug.WriteLine("Recent project #" + recentIndex); - mUI.OpenRecentProject(recentIndex); + mMainCtrl.OpenRecentProject(recentIndex); } private void CodeListView_SelectionChanged(object sender, SelectionChangedEventArgs e) { diff --git a/SourceGenWPF/ProjectFile.cs b/SourceGenWPF/ProjectFile.cs index 2f2ecf5..98108bb 100644 --- a/SourceGenWPF/ProjectFile.cs +++ b/SourceGenWPF/ProjectFile.cs @@ -721,7 +721,7 @@ namespace SourceGenWPF { // Shouldn't allow DisplayList.Line.HEADER_COMMENT_OFFSET on anything but // LongComment. Maybe "bool allowNegativeKeys"? if (intKey < fileLen && - (intKey >= 0 || intKey == DisplayListGen.Line.HEADER_COMMENT_OFFSET)) { + (intKey >= 0 || intKey == LineListGen.Line.HEADER_COMMENT_OFFSET)) { return true; } else { report.Add(FileLoadItem.Type.Warning, diff --git a/SourceGenWPF/Res/Strings.xaml b/SourceGenWPF/Res/Strings.xaml index d0a5760..722af32 100644 --- a/SourceGenWPF/Res/Strings.xaml +++ b/SourceGenWPF/Res/Strings.xaml @@ -26,6 +26,7 @@ C# Source Files(*.cs)|*.cs SourceGen projects(*.dis65)|*.dis65 SourceGen symbols (*.sym65)|*.sym65 + Target assembler: {0} v{1} [{2}] Extension scripts: Default settings: Symbol files: @@ -39,6 +40,8 @@ The file is {0:N0} bytes long, but the project expected {1:N0}. The file has CRC {0}, but the project expected {1}. Failed + Executing assembler... + Generating {0}... comment long comment note @@ -47,5 +50,7 @@ type hint user-defined label This project was created by a newer version of SourceGen. It may contain data that will be lost if the project is edited. + The RuntimeData directory was not found. It should be in the same directory as the executable. + RuntimeData Not Found {1} CPU @ {2} MHz \ No newline at end of file diff --git a/SourceGenWPF/Res/Strings.xaml.cs b/SourceGenWPF/Res/Strings.xaml.cs index 0050d3c..616e32b 100644 --- a/SourceGenWPF/Res/Strings.xaml.cs +++ b/SourceGenWPF/Res/Strings.xaml.cs @@ -64,6 +64,8 @@ namespace SourceGenWPF.Res { (string)Application.Current.FindResource("str_FileFilterDis65"); public static string FILE_FILTER_SYM65 = (string)Application.Current.FindResource("str_FileFilterSym65"); + public static string GENERATED_FOR_VERSION_FMT = + (string)Application.Current.FindResource("str_GeneratedForVersion"); public static string INITIAL_EXTENSION_SCRIPTS = (string)Application.Current.FindResource("str_InitialExtensionScripts"); public static string INITIAL_PARAMETERS = @@ -90,6 +92,10 @@ namespace SourceGenWPF.Res { (string)Application.Current.FindResource("str_OpenDataWrongLengthFmt"); public static string OPERATION_FAILED = (string)Application.Current.FindResource("str_OperationFailed"); + public static string PROGRESS_ASSEMBLING = + (string)Application.Current.FindResource("str_ProgressAssembling"); + public static string PROGRESS_GENERATING_FMT = + (string)Application.Current.FindResource("str_ProgressGeneratingFmt"); public static string PROJECT_FIELD_COMMENT = (string)Application.Current.FindResource("str_ProjectFieldComment"); public static string PROJECT_FIELD_LONG_COMMENT = @@ -106,6 +112,10 @@ namespace SourceGenWPF.Res { (string)Application.Current.FindResource("str_ProjectFieldUserLabel"); public static string PROJECT_FROM_NEWER_APP = (string)Application.Current.FindResource("str_ProjectFromNewerApp"); + public static string RUNTIME_DIR_NOT_FOUND = + (string)Application.Current.FindResource("str_RuntimeDirNotFound"); + public static string RUNTIME_DIR_NOT_FOUND_CAPTION = + (string)Application.Current.FindResource("str_RuntimeDirNotFoundCaption"); public static string SETUP_SYSTEM_SUMMARY_FMT = (string)Application.Current.FindResource("str_SetupSystemSummaryFmt"); } diff --git a/SourceGenWPF/SourceGenWPF.csproj b/SourceGenWPF/SourceGenWPF.csproj index 3554142..1bb2e95 100644 --- a/SourceGenWPF/SourceGenWPF.csproj +++ b/SourceGenWPF/SourceGenWPF.csproj @@ -62,6 +62,17 @@ App.xaml Code + + + + + + + + + + + @@ -97,7 +108,7 @@ - +