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 @@
-
+