From 1ad9caa783e2240eee8c103e79c52a480f5042cf Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Sat, 3 Aug 2019 20:54:07 -0700 Subject: [PATCH] First pass at ACME support I managed to work around most of the quirks, but there's still an issue with 65816 code. Also, enabled word wrapping in the AsmGen text boxes. --- Asm65/Formatter.cs | 18 +- SourceGen/AsmGen/AsmAcme.cs | 746 ++++++++++++++++++++++ SourceGen/AsmGen/AsmCc65.cs | 14 +- SourceGen/AsmGen/AsmMerlin32.cs | 1 + SourceGen/AsmGen/AsmTass64.cs | 3 +- SourceGen/AsmGen/AssemblerInfo.cs | 4 +- SourceGen/AsmGen/GenCommon.cs | 37 +- SourceGen/AsmGen/IGenerator.cs | 8 +- SourceGen/AsmGen/WpfGui/GenAndAsm.xaml | 6 +- SourceGen/SourceGen.csproj | 1 + SourceGen/Tests/WpfGui/GenTestRunner.xaml | 2 +- 11 files changed, 808 insertions(+), 32 deletions(-) create mode 100644 SourceGen/AsmGen/AsmAcme.cs diff --git a/Asm65/Formatter.cs b/Asm65/Formatter.cs index ea33c4a..6155827 100644 --- a/Asm65/Formatter.cs +++ b/Asm65/Formatter.cs @@ -59,6 +59,7 @@ namespace Asm65 { // functional changes to assembly output public bool mSuppressHexNotation; // omit '$' before hex digits + public bool mSuppressImpliedAcc; // emit just "LSR" rather than "LSR A"? public bool mAllowHighAsciiCharConst; // can we do high-ASCII character constants? // (this might need to be generalized) @@ -66,6 +67,7 @@ namespace Asm65 { public string mForceDirectOperandPrefix; // these may be null or empty public string mForceAbsOpcodeSuffix; public string mForceAbsOperandPrefix; + public string mForceDirectOpcodeSuffix; public string mForceLongOpcodeSuffix; public string mForceLongOperandPrefix; @@ -117,7 +119,7 @@ namespace Asm65 { // Bits and pieces. char mHexFmtChar; string mHexPrefix; - char mAccChar; + string mAccChar; char mXregChar; char mYregChar; char mSregChar; @@ -250,9 +252,9 @@ namespace Asm65 { mHexPrefix = "$"; } if (mFormatConfig.mUpperOperandA) { - mAccChar = 'A'; + mAccChar = "A"; } else { - mAccChar = 'a'; + mAccChar = "a"; } if (mFormatConfig.mUpperOperandXY) { mXregChar = 'X'; @@ -487,7 +489,9 @@ namespace Asm65 { public string FormatMnemonic(string mnemonic, OpDef.WidthDisambiguation wdis) { string opcodeStr = mnemonic; if (wdis == OpDef.WidthDisambiguation.ForceDirect) { - // nothing to do for opcode + if (!string.IsNullOrEmpty(mFormatConfig.mForceDirectOpcodeSuffix)) { + opcodeStr += mFormatConfig.mForceDirectOpcodeSuffix; + } } else if (wdis == OpDef.WidthDisambiguation.ForceAbs) { if (!string.IsNullOrEmpty(mFormatConfig.mForceAbsOpcodeSuffix)) { opcodeStr += mFormatConfig.mForceAbsOpcodeSuffix; @@ -573,7 +577,11 @@ namespace Asm65 { fmt = "[{0}]"; break; case AddressMode.Acc: - fmt = "" + mAccChar; + if (mFormatConfig.mSuppressImpliedAcc) { + fmt = string.Empty; + } else { + fmt = mAccChar; + } break; case AddressMode.DPIndIndexY: fmt = "({0})," + mYregChar; diff --git a/SourceGen/AsmGen/AsmAcme.cs b/SourceGen/AsmGen/AsmAcme.cs new file mode 100644 index 0000000..733c37c --- /dev/null +++ b/SourceGen/AsmGen/AsmAcme.cs @@ -0,0 +1,746 @@ +/* + * 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 SourceGen.AsmGen { + #region IGenerator + + /// + /// Generate source code compatible with the ACME assembler + /// (https://sourceforge.net/projects/acme-crossass/). + /// + public class GenAcme : IGenerator { + private const string ASM_FILE_SUFFIX = "_acme.a"; // must start with underscore + private const int MAX_OPERAND_LEN = 64; + private const string CLOSE_PSEUDOPC = "} ;!pseudopc"; + + // 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; + + // Version we're coded against. + private static CommonUtil.Version V0_96_4 = new CommonUtil.Version(0, 96, 4); + + // Set if we're inside a "pseudopc" block, which will need to be closed. + private bool mInPseudoPcBlock = false; + + + // Pseudo-op string constants. + private static PseudoOp.PseudoOpNames sDataOpNames = new PseudoOp.PseudoOpNames() { + EquDirective = "=", + OrgDirective = "!pseudopc", + //RegWidthDirective // !al, !as, !rl, !rs + DefineData1 = "!byte", + DefineData2 = "!word", + DefineData3 = "!24", + DefineData4 = "!32", + //DefineBigData2 + //DefineBigData3 + //DefineBigData4 + Fill = "!fill", + Dense = "!hex", + StrGeneric = "!text", // can use !xor for high ASCII + //StrReverse + //StrNullTerm + //StrLen8 + //StrLen16 + //StrDci + //StrDciReverse + }; + + + // IGenerator + public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps, + out Formatter.FormatConfig formatConfig) { + pseudoOps = sDataOpNames.GetCopy(); + + 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; + + // ACME isn't a single-pass assembler, but the code that determines label widths + // only runs in the first pass and doesn't get corrected. So unlike cc65, which + // generates correct zero-page acceses once the label's value is known, ACME + // uses 16-bit addressing to zero-page labels for backward references if there + // are any forward references at all. The easy way to deal with this is to make + // all zero-page label references have explicit widths. + // + // Example: + // * = $1000 + // jmp zero + // !pseudopc $0000 { + // zero nop + // lda zero + // rts + // } + Quirks = new AssemblerQuirks(); + Quirks.SinglePassAssembler = true; + Quirks.SinglePassNoLabelCorrection = true; + + mWorkDirectory = workDirectory; + mFileNameBase = fileNameBase; + Settings = settings; + + mLongLabelNewLine = Settings.GetBool(AppSettings.SRCGEN_LONG_LABEL_NEW_LINE, false); + + AssemblerConfig config = AssemblerConfig.GetConfig(settings, + AssemblerInfo.Id.Acme); + mColumnWidths = (int[])config.ColumnWidths.Clone(); + } + + /// + /// Configures the assembler-specific format items. + /// + private void SetFormatConfigValues(ref Formatter.FormatConfig config) { + config.mSuppressImpliedAcc = true; + + config.mForceDirectOpcodeSuffix = "+1"; + config.mForceAbsOpcodeSuffix = "+2"; + config.mForceLongOpcodeSuffix = "+3"; + config.mForceDirectOperandPrefix = string.Empty; + config.mForceAbsOperandPrefix = string.Empty; + config.mForceLongOperandPrefix = string.Empty; + 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(); + } + + // 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, + "acme", V0_96_4, AsmAcme.OPTIONS)); + } + + GenCommon.Generate(this, sw, worker); + + if (mInPseudoPcBlock) { + OutputLine(string.Empty, CLOSE_PSEUDOPC, 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 = "6510"; + } 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.OpALR_Imm) { + // ACME wants "ASR" instead for $4b + return "asr"; + } else if (op == OpDef.OpLAX_Imm) { + // ACME spits out an error on $ab + return null; + } + } + if (op == OpDef.OpWDM_WDM) { + // ACME doesn't like this 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; + 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; + } + } + + /// + /// 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) { + // For the first one, set the "real" PC. For all subsequent directives, set the + // "pseudo" PC. + if (offset == 0) { + OutputLine("*", "=", SourceFormatter.FormatHexValue(address, 4), string.Empty); + } else { + if (mInPseudoPcBlock) { + // close previous block + OutputLine(string.Empty, CLOSE_PSEUDOPC, string.Empty, string.Empty); + } + OutputLine(string.Empty, sDataOpNames.OrgDirective, + SourceFormatter.FormatHexValue(address, 4) + " {", string.Empty); + mInPseudoPcBlock = 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) ? "!rl" : "!rs"; + 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. + // + // We could probably do something fancy with !xor to + // make high-ASCII work nicely. + + 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: + case FormatDescriptor.SubType.L8String: + 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 AsmAcme : IAssembler { + public const string OPTIONS = ""; + + // 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 = "ACME Assembler"; + exeName = "acme"; + } + + // 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.Acme); + 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: "This is ACME, release 0.96.4 ("Fenchurch"), 22 Dec 2017 ..." + // Linux - Stderr: "This is ACME, release 0.96.4 ("Fenchurch"), 20 Apr 2019 ..." + + const string PREFIX = "release "; + string str = cmd.Stdout; + int start = str.IndexOf(PREFIX); + int end = (start < 0) ? -1 : str.IndexOf(' ', start + PREFIX.Length + 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.Acme); + if (string.IsNullOrEmpty(config.ExecutablePath)) { + Debug.WriteLine("Assembler not configured"); + return null; + } + + worker.ReportProgress(0, Res.Strings.PROGRESS_ASSEMBLING); + + // Output file name is source file name with the ".a". + 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 + " -o \"" + outFileName + "\"" + " \"" + pathName + "\"" , + mWorkDirectory, null); + cmd.Execute(); + + // Can't really do anything with a "cancel" request. + + // Output filename is the input filename without the ".a". 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/SourceGen/AsmGen/AsmCc65.cs b/SourceGen/AsmGen/AsmCc65.cs index a86e1d0..96ebdab 100644 --- a/SourceGen/AsmGen/AsmCc65.cs +++ b/SourceGen/AsmGen/AsmCc65.cs @@ -125,18 +125,7 @@ namespace SourceGen.AsmGen { // 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", - }; + pseudoOps = sDataOpNames.GetCopy(); formatConfig = new Formatter.FormatConfig(); SetFormatConfigValues(ref formatConfig); @@ -182,6 +171,7 @@ namespace SourceGen.AsmGen { /// Configures the assembler-specific format items. /// private void SetFormatConfigValues(ref Formatter.FormatConfig config) { + config.mForceDirectOpcodeSuffix = string.Empty; config.mForceAbsOpcodeSuffix = string.Empty; config.mForceLongOpcodeSuffix = string.Empty; config.mForceDirectOperandPrefix = "z:"; // zero diff --git a/SourceGen/AsmGen/AsmMerlin32.cs b/SourceGen/AsmGen/AsmMerlin32.cs index 5b6c933..a785056 100644 --- a/SourceGen/AsmGen/AsmMerlin32.cs +++ b/SourceGen/AsmGen/AsmMerlin32.cs @@ -183,6 +183,7 @@ namespace SourceGen.AsmGen { /// Configures the assembler-specific format items. /// private void SetFormatConfigValues(ref Formatter.FormatConfig config) { + config.mForceDirectOpcodeSuffix = string.Empty; config.mForceAbsOpcodeSuffix = ":"; config.mForceLongOpcodeSuffix = "l"; config.mForceDirectOperandPrefix = string.Empty; diff --git a/SourceGen/AsmGen/AsmTass64.cs b/SourceGen/AsmGen/AsmTass64.cs index 149a00c..9f671db 100644 --- a/SourceGen/AsmGen/AsmTass64.cs +++ b/SourceGen/AsmGen/AsmTass64.cs @@ -137,7 +137,7 @@ namespace SourceGen.AsmGen { // IGenerator public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps, out Formatter.FormatConfig formatConfig) { - pseudoOps = sDataOpNames; + pseudoOps = sDataOpNames.GetCopy(); formatConfig = new Formatter.FormatConfig(); SetFormatConfigValues(ref formatConfig); @@ -177,6 +177,7 @@ namespace SourceGen.AsmGen { config.mBankSelectBackQuote = true; + config.mForceDirectOpcodeSuffix = string.Empty; config.mForceAbsOpcodeSuffix = string.Empty; config.mForceLongOpcodeSuffix = string.Empty; config.mForceDirectOperandPrefix = string.Empty; diff --git a/SourceGen/AsmGen/AssemblerInfo.cs b/SourceGen/AsmGen/AssemblerInfo.cs index 39a6144..37ed855 100644 --- a/SourceGen/AsmGen/AssemblerInfo.cs +++ b/SourceGen/AsmGen/AssemblerInfo.cs @@ -32,6 +32,7 @@ namespace SourceGen.AsmGen { public enum Id { Unknown = 0, Tass64, + Acme, Cc65, Merlin32, } @@ -44,6 +45,7 @@ namespace SourceGen.AsmGen { private static AssemblerInfo[] sInfo = new AssemblerInfo[] { new AssemblerInfo(Id.Unknown, "???", null, null), new AssemblerInfo(Id.Tass64, "64tass", typeof(GenTass64), typeof(AsmTass64)), + new AssemblerInfo(Id.Acme, "ACME", typeof(GenAcme), typeof(AsmAcme)), new AssemblerInfo(Id.Cc65, "cc65", typeof(GenCc65), typeof(AsmCc65)), new AssemblerInfo(Id.Merlin32, "Merlin 32", typeof(GenMerlin32), typeof(AsmMerlin32)), }; @@ -165,7 +167,7 @@ namespace SourceGen.AsmGen { public override string ToString() { - return "Asm " + ((int)AssemblerId).ToString() + ": " + Name; + return "AsmInfo " + ((int)AssemblerId).ToString() + ": " + Name; } } } diff --git a/SourceGen/AsmGen/GenCommon.cs b/SourceGen/AsmGen/GenCommon.cs index 798850a..5f4cc53 100644 --- a/SourceGen/AsmGen/GenCommon.cs +++ b/SourceGen/AsmGen/GenCommon.cs @@ -175,8 +175,10 @@ namespace SourceGen.AsmGen { (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)) { + // Could be a forward reference to a direct-page label. For ACME, we don't + // care if it's forward or not. + if ((gen.Quirks.SinglePassNoLabelCorrection && IsLabelReference(gen, offset)) || + IsForwardLabelReference(gen, offset)) { wdis = OpDef.WidthDisambiguation.ForceDirect; } } @@ -315,19 +317,36 @@ namespace SourceGen.AsmGen { /// 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) { + return (GetLabelOffsetFromOperand(gen, offset) > offset); + } + + /// + /// Determines whether the instruction at the specified offset has an operand + /// that references a symbol. + /// + /// Source generator reference. + /// Offset of instruction opcode. + /// True if the instruction's operand is a forward reference to a label. + private static bool IsLabelReference(IGenerator gen, int offset) { + return (GetLabelOffsetFromOperand(gen, offset) >= 0); + } + + /// + /// Determines the offset of the label that the operand's symbol references. + /// + /// Source generator reference. + /// Offset of instruction opcode. + /// The offset of the label, or -1 if the operand isn't a symbolic reference + /// to a known label. + private static int GetLabelOffsetFromOperand(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; + return -1; } - int labelOffset = proj.FindLabelOffsetByName(dfd.SymbolRef.Label); - if (labelOffset <= offset) { - // Doesn't exist, or is backward reference. - return false; - } - return true; + return proj.FindLabelOffsetByName(dfd.SymbolRef.Label); } /// diff --git a/SourceGen/AsmGen/IGenerator.cs b/SourceGen/AsmGen/IGenerator.cs index 55ce419..eda5e5c 100644 --- a/SourceGen/AsmGen/IGenerator.cs +++ b/SourceGen/AsmGen/IGenerator.cs @@ -185,8 +185,14 @@ namespace SourceGen.AsmGen { public bool NoPcRelBankWrap { get; set; } /// - /// Is the assembler implemented as a single pass? + /// Is the assembler implemented as a single pass? (e.g. cc65) /// public bool SinglePassAssembler { get; set; } + + /// + /// Is the assembler's label width determination performed only in the first pass, + /// and not corrected when the actual width is determined? + /// + public bool SinglePassNoLabelCorrection { get; set; } } } \ No newline at end of file diff --git a/SourceGen/AsmGen/WpfGui/GenAndAsm.xaml b/SourceGen/AsmGen/WpfGui/GenAndAsm.xaml index 78e9a11..1f05107 100644 --- a/SourceGen/AsmGen/WpfGui/GenAndAsm.xaml +++ b/SourceGen/AsmGen/WpfGui/GenAndAsm.xaml @@ -80,7 +80,8 @@ limitations under the License. + FontFamily="{StaticResource GeneralMonoFont}" VerticalScrollBarVisibility="Auto" + TextWrapping="Wrap"> sample text1 @@ -96,7 +97,8 @@ limitations under the License. + FontFamily="{StaticResource GeneralMonoFont}" VerticalScrollBarVisibility="Auto" + TextWrapping="Wrap"> sample text2 diff --git a/SourceGen/SourceGen.csproj b/SourceGen/SourceGen.csproj index f477fb0..619e1fd 100644 --- a/SourceGen/SourceGen.csproj +++ b/SourceGen/SourceGen.csproj @@ -62,6 +62,7 @@ App.xaml Code + diff --git a/SourceGen/Tests/WpfGui/GenTestRunner.xaml b/SourceGen/Tests/WpfGui/GenTestRunner.xaml index 45739ad..27e602a 100644 --- a/SourceGen/Tests/WpfGui/GenTestRunner.xaml +++ b/SourceGen/Tests/WpfGui/GenTestRunner.xaml @@ -24,7 +24,7 @@ limitations under the License. mc:Ignorable="d" Title="Source Generation Test" Width="640" Height="480" MinWidth="640" MinHeight="480" - ShowInTaskbar="False" WindowStartupLocation="CenterOwner" + ShowInTaskbar="False" WindowStartupLocation="CenterOwner" ResizeMode="CanResizeWithGrip" Closing="Window_Closing">