From 4902b89cf8315ad3352a49b8a83b5519ef104c8b Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Sat, 17 Aug 2019 11:14:05 -0700 Subject: [PATCH] Various improvements The PseudoOpNames class is increasingly being used in situations where mutability is undesirable. This change makes instances immutable, eliminating the Copy() method and adding a constructor that takes a Dictionary. The serialization code now operates on a Dictionary instead of the class properties, but the JSON encoding is identical, so this doesn't invalidate app settings file data. Added an equality test to PseudoOpNames. In LineListGen, don't reset the line list if the names haven't actually changed. Use a table lookup for C64 character conversions. I figure that should be faster than multiple conditionals on a modern x64 system. Fixed a 64tass generator issue where we tried to query project properties in a call that might not have a project available (specifically, getting FormatConfig values out of the generator for use in the "quick set" buttons for Display Format). Fixed a regression test harness issue where, if the assembler reported success but didn't actually generate output, an exception would be thrown that halted the tests. Increased the width of text entry fields on the Pseudo-Op tab of app settings. The previous 8-character limit wasn't wide enough to hold ACME's "!pseudopc". Also, use TrimEnd() to remove trailing spaces (leading spaces are still allowed). In the last couple of months, Win10 started stalling for a fraction of a second when executing assemblers. It doesn't do this every time; mostly it happens if it has been a while since the assembler was run. My guess is this has to do with changes to the built-in malware scanner. Whatever the case, we now change the mouse pointer to a wait cursor while updating the assembler version cache. --- Asm65/CharEncoding.cs | 66 ++++++--- SourceGen/AsmGen/AsmAcme.cs | 45 ++++--- SourceGen/AsmGen/AsmCc65.cs | 45 ++++--- SourceGen/AsmGen/AsmMerlin32.cs | 55 ++++---- SourceGen/AsmGen/AsmTass64.cs | 91 +++++++------ SourceGen/AsmGen/IGenerator.cs | 5 +- SourceGen/DisplayList.cs | 1 + SourceGen/FormatDescriptor.cs | 7 +- SourceGen/LineListGen.cs | 7 +- SourceGen/MainController.cs | 6 +- SourceGen/PseudoOp.cs | 165 ++++++++++++++++------- SourceGen/Tests/GenTest.cs | 9 +- SourceGen/WpfGui/EditAppSettings.xaml | 32 ++--- SourceGen/WpfGui/EditAppSettings.xaml.cs | 28 ++-- 14 files changed, 341 insertions(+), 221 deletions(-) diff --git a/Asm65/CharEncoding.cs b/Asm65/CharEncoding.cs index 76134e1..4851cdb 100644 --- a/Asm65/CharEncoding.cs +++ b/Asm65/CharEncoding.cs @@ -193,17 +193,29 @@ namespace Asm65 { public static bool IsExtendedC64Petscii(byte val) { return sExtendedPetscii[val]; } - public static char ConvertC64Petscii(byte val) { - if ((val >= 0x20 && val <= 0x40) || val == 0x5b || val == 0x5d) { - return (char)val; // number/symbols, '[', ']' - } else if (val >= 0x41 && val <= 0x5a) { - return (char)(val + 0x20); // lower case - } else if (val >= 0xc1 && val <= 0xda) { - return (char)(val - 0x80); // upper case - } else { - Debug.Assert(!IsPrintableC64Petscii(val)); - return UNPRINTABLE_CHAR; + private static char[] sPetsciiToUnicode = CreatePetsciiToUnicodeMap(); + private static char[] CreatePetsciiToUnicodeMap() { + // There are performance arguments for doing this with and without a table. For + // x64 with fast memory and large caches, table seems reasonable. + char[] map = new char[256]; + for (int val = 0; val < 256; val++) { + char ch; + if ((val >= 0x20 && val <= 0x40) || val == 0x5b || val == 0x5d) { + ch = (char)val; // number/symbols, '[', ']' + } else if (val >= 0x41 && val <= 0x5a) { + ch = (char)(val + 0x20); // lower case + } else if (val >= 0xc1 && val <= 0xda) { + ch = (char)(val - 0x80); // upper case + } else { + Debug.Assert(!IsPrintableC64Petscii((byte)val)); + ch = UNPRINTABLE_CHAR; + } + map[val] = ch; } + return map; + } + public static char ConvertC64Petscii(byte val) { + return sPetsciiToUnicode[val]; } // @@ -243,19 +255,29 @@ namespace Asm65 { public static bool IsExtendedC64ScreenCode(byte val) { return sPrintableScreenCode[val]; } - public static char ConvertC64ScreenCode(byte val) { - if (val == 0x00 || val == 0x1b || val == 0x1d) { - return (char)(val + 0x40); // '@', '[', ']' - } else if (val >= 0x01 && val <= 0x1a) { - return (char)(val + 0x60); // lower case - } else if (val >= 0x20 && val <= 0x3f) { - return (char)(val); // numbers/symbols - } else if (val >= 0x41 && val <= 0x5a) { - return (char)(val); // upper case - } else { - Debug.Assert(!IsPrintableC64ScreenCode(val)); - return UNPRINTABLE_CHAR; + private static char[] sScreenCodeToUnicode = CreateScreenCodeToUnicodeMap(); + private static char[] CreateScreenCodeToUnicodeMap() { + char[] map = new char[256]; + for (int val = 0; val < 256; val++) { + char ch; + if (val == 0x00 || val == 0x1b || val == 0x1d) { + ch = (char)(val + 0x40); // '@', '[', ']' + } else if (val >= 0x01 && val <= 0x1a) { + ch = (char)(val + 0x60); // lower case + } else if (val >= 0x20 && val <= 0x3f) { + ch = (char)(val); // numbers/symbols + } else if (val >= 0x41 && val <= 0x5a) { + ch = (char)(val); // upper case + } else { + Debug.Assert(!IsPrintableC64ScreenCode((byte)val)); + ch = UNPRINTABLE_CHAR; + } + map[val] = ch; } + return map; + } + public static char ConvertC64ScreenCode(byte val) { + return sScreenCodeToUnicode[val]; } public static char ConvertLowAndHighC64ScreenCode(byte val) { return ConvertC64ScreenCode((byte)(val & 0x7f)); diff --git a/SourceGen/AsmGen/AsmAcme.cs b/SourceGen/AsmGen/AsmAcme.cs index 01de707..393fb3b 100644 --- a/SourceGen/AsmGen/AsmAcme.cs +++ b/SourceGen/AsmGen/AsmAcme.cs @@ -103,32 +103,33 @@ namespace SourceGen.AsmGen { // 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 - }; + private static PseudoOp.PseudoOpNames sDataOpNames = + new PseudoOp.PseudoOpNames(new Dictionary { + { "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 + }); // IGenerator public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps, out Formatter.FormatConfig formatConfig) { - pseudoOps = sDataOpNames.GetCopy(); + pseudoOps = sDataOpNames; formatConfig = new Formatter.FormatConfig(); SetFormatConfigValues(ref formatConfig); @@ -374,7 +375,7 @@ namespace SourceGen.AsmGen { break; case FormatDescriptor.Type.NumericBE: opcodeStr = sDataOpNames.GetDefineBigData(length); - if (opcodeStr == null) { + if (string.IsNullOrEmpty(opcodeStr)) { // Nothing defined, output as comma-separated single-byte values. GenerateShortSequence(offset, length, out opcodeStr, out operandStr); } else { diff --git a/SourceGen/AsmGen/AsmCc65.cs b/SourceGen/AsmGen/AsmCc65.cs index c6bb871..e8fe0ce 100644 --- a/SourceGen/AsmGen/AsmCc65.cs +++ b/SourceGen/AsmGen/AsmCc65.cs @@ -100,32 +100,33 @@ namespace SourceGen.AsmGen { // 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 - }; + private static PseudoOp.PseudoOpNames sDataOpNames = + new PseudoOp.PseudoOpNames(new Dictionary { + { "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 + }); // IGenerator public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps, out Formatter.FormatConfig formatConfig) { - pseudoOps = sDataOpNames.GetCopy(); + pseudoOps = sDataOpNames; formatConfig = new Formatter.FormatConfig(); SetFormatConfigValues(ref formatConfig); @@ -401,7 +402,7 @@ namespace SourceGen.AsmGen { break; case FormatDescriptor.Type.NumericBE: opcodeStr = sDataOpNames.GetDefineBigData(length); - if (opcodeStr == null) { + if ((string.IsNullOrEmpty(opcodeStr))) { // Nothing defined, output as comma-separated single-byte values. GenerateShortSequence(offset, length, out opcodeStr, out operandStr); } else { diff --git a/SourceGen/AsmGen/AsmMerlin32.cs b/SourceGen/AsmGen/AsmMerlin32.cs index 5d21765..d0a8300 100644 --- a/SourceGen/AsmGen/AsmMerlin32.cs +++ b/SourceGen/AsmGen/AsmMerlin32.cs @@ -90,37 +90,35 @@ namespace SourceGen.AsmGen { 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", - StrReverse = "rev", - //StrNullTerm - StrLen8 = "str", - StrLen16 = "strl", - StrDci = "dci", - }; + // Pseudo-op string constants. + private static PseudoOp.PseudoOpNames sDataOpNames = + new PseudoOp.PseudoOpNames(new Dictionary { + { "EquDirective", "equ" }, + { "OrgDirective", "org" }, + //RegWidthDirective + { "DefineData1", "dfb" }, + { "DefineData2", "dw" }, + { "DefineData3", "adr" }, + { "DefineData4", "adrl" }, + { "DefineBigData2", "ddb" }, + //DefineBigData3 + //DefineBigData4 + { "Fill", "ds" }, + { "Dense", "hex" }, + { "StrGeneric", "asc" }, + { "StrReverse", "rev" }, + //StrNullTerm + { "StrLen8", "str" }, + { "StrLen16", "strl" }, + { "StrDci", "dci" }, + }); + private const string REG_WIDTH_DIRECTIVE = "mx"; // IGenerator public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps, out Formatter.FormatConfig formatConfig) { - pseudoOps = sDataOpNames.GetCopy(); - pseudoOps.RegWidthDirective = string.Empty; + pseudoOps = sDataOpNames; formatConfig = new Formatter.FormatConfig(); SetFormatConfigValues(ref formatConfig); @@ -249,7 +247,7 @@ namespace SourceGen.AsmGen { break; case FormatDescriptor.Type.NumericBE: opcodeStr = sDataOpNames.GetDefineBigData(length); - if (opcodeStr == null) { + if ((string.IsNullOrEmpty(opcodeStr))) { // Nothing defined, output as comma-separated single-byte values. GenerateShortSequence(offset, length, out opcodeStr, out operandStr); } else { @@ -413,8 +411,7 @@ namespace SourceGen.AsmGen { // Assembler defaults to short regs, so we can skip this. return; } - OutputLine(string.Empty, - SourceFormatter.FormatPseudoOp(sDataOpNames.RegWidthDirective), + OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(REG_WIDTH_DIRECTIVE), "%" + newM + newX, string.Empty); } diff --git a/SourceGen/AsmGen/AsmTass64.cs b/SourceGen/AsmGen/AsmTass64.cs index 7c1fa3d..395b97c 100644 --- a/SourceGen/AsmGen/AsmTass64.cs +++ b/SourceGen/AsmGen/AsmTass64.cs @@ -111,33 +111,34 @@ namespace SourceGen.AsmGen { // 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 = ".shift" - }; + private static PseudoOp.PseudoOpNames sDataOpNames = + new PseudoOp.PseudoOpNames(new Dictionary { + { "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", ".shift" } + }); private const string HERE_PSEUDO_OP = ".here"; // IGenerator public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps, out Formatter.FormatConfig formatConfig) { - pseudoOps = sDataOpNames.GetCopy(); + pseudoOps = sDataOpNames; formatConfig = new Formatter.FormatConfig(); SetFormatConfigValues(ref formatConfig); @@ -165,7 +166,7 @@ namespace SourceGen.AsmGen { } /// - /// Configures the assembler-specific format items. + /// Configures the assembler-specific format items. May be called without a Project. /// private void SetFormatConfigValues(ref Formatter.FormatConfig config) { // Must be lower case when --case-sensitive is used. @@ -187,27 +188,6 @@ namespace SourceGen.AsmGen { config.mFullLineCommentDelimiterBase = ";"; config.mBoxLineCommentDelimiter = ";"; config.mExpressionMode = Formatter.FormatConfig.ExpressionMode.Common; - - // Configure delimiters for single-character operands. The conversion mode we - // use is determined by the default text mode in the project properties. - Formatter.DelimiterSet charSet = new Formatter.DelimiterSet(); - TextScanMode textMode = Project.ProjectProps.AnalysisParams.DefaultTextScanMode; - switch (textMode) { - case TextScanMode.C64Petscii: - charSet.Set(CharEncoding.Encoding.C64Petscii, Formatter.SINGLE_QUOTE_DELIM); - break; - case TextScanMode.C64ScreenCode: - charSet.Set(CharEncoding.Encoding.C64ScreenCode, Formatter.SINGLE_QUOTE_DELIM); - break; - case TextScanMode.LowAscii: - case TextScanMode.LowHighAscii: - default: - charSet.Set(CharEncoding.Encoding.Ascii, Formatter.SINGLE_QUOTE_DELIM); - charSet.Set(CharEncoding.Encoding.HighAscii, - new Formatter.DelimiterDef(string.Empty, '\'', '\'', " | $80")); - break; - } - config.mCharDelimiters = charSet; } // IGenerator @@ -221,6 +201,29 @@ namespace SourceGen.AsmGen { Formatter.FormatConfig config = new Formatter.FormatConfig(); GenCommon.ConfigureFormatterFromSettings(Settings, ref config); SetFormatConfigValues(ref config); + + // Configure delimiters for single-character operands. The conversion mode we + // use is determined by the default text mode in the project properties. + Formatter.DelimiterSet charSet = new Formatter.DelimiterSet(); + TextScanMode textMode = Project.ProjectProps.AnalysisParams.DefaultTextScanMode; + switch (textMode) { + case TextScanMode.C64Petscii: + charSet.Set(CharEncoding.Encoding.C64Petscii, + Formatter.SINGLE_QUOTE_DELIM); + break; + case TextScanMode.C64ScreenCode: + charSet.Set(CharEncoding.Encoding.C64ScreenCode, + Formatter.SINGLE_QUOTE_DELIM); + break; + case TextScanMode.LowAscii: + case TextScanMode.LowHighAscii: + default: + charSet.Set(CharEncoding.Encoding.Ascii, Formatter.SINGLE_QUOTE_DELIM); + charSet.Set(CharEncoding.Encoding.HighAscii, + new Formatter.DelimiterDef(string.Empty, '\'', '\'', " | $80")); + break; + } + config.mCharDelimiters = charSet; SourceFormatter = new Formatter(config); string msg = string.Format(Res.Strings.PROGRESS_GENERATING_FMT, pathName); @@ -387,7 +390,7 @@ namespace SourceGen.AsmGen { break; case FormatDescriptor.Type.NumericBE: opcodeStr = sDataOpNames.GetDefineBigData(length); - if (opcodeStr == null) { + if ((string.IsNullOrEmpty(opcodeStr))) { // Nothing defined, output as comma-separated single-byte values. GenerateShortSequence(offset, length, out opcodeStr, out operandStr); } else { diff --git a/SourceGen/AsmGen/IGenerator.cs b/SourceGen/AsmGen/IGenerator.cs index 083f8b6..5172b36 100644 --- a/SourceGen/AsmGen/IGenerator.cs +++ b/SourceGen/AsmGen/IGenerator.cs @@ -28,8 +28,9 @@ namespace SourceGen.AsmGen { /// 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. + /// + /// This may be called on an unconfigured IGenerator, so this should not expect to + /// have access to project properties. /// /// Table of pseudo-op names. /// Format configuration. diff --git a/SourceGen/DisplayList.cs b/SourceGen/DisplayList.cs index 56ce2d8..c161ff0 100644 --- a/SourceGen/DisplayList.cs +++ b/SourceGen/DisplayList.cs @@ -50,6 +50,7 @@ namespace SourceGen { /// https://www.codeproject.com/Articles/34405/WPF-Data-Virtualization?msg=5635751 /// https://web.archive.org/web/20121216034305/http://www.zagstudio.com/blog/498 /// https://web.archive.org/web/20121107200359/http://www.zagstudio.com/blog/378 + /// https://github.com/lvaleriu/Virtualization /// public class DisplayList : IList, IList, INotifyCollectionChanged, INotifyPropertyChanged { diff --git a/SourceGen/FormatDescriptor.cs b/SourceGen/FormatDescriptor.cs index 55c6ee2..6e9ce28 100644 --- a/SourceGen/FormatDescriptor.cs +++ b/SourceGen/FormatDescriptor.cs @@ -32,9 +32,10 @@ namespace SourceGen { /// There may be a large number of these, so try to keep the size down. These are usually /// stored in lists, not arrays, so declaring as a struct wouldn't help with that. /// - /// The stringified names of the enum values are currently serialized into the project - /// file. DO NOT rename members of the enumeration without creating an upgrade path. If - /// new values are added, the project file version number should be incremented. + /// IMPORTANT: The stringified names of the enum values are currently serialized into + /// the project file. DO NOT rename members of the enumerations without creating an + /// upgrade path. If new values are added, the project file version number should + /// be incremented. /// public class FormatDescriptor { /// diff --git a/SourceGen/LineListGen.cs b/SourceGen/LineListGen.cs index f55e33c..f195304 100644 --- a/SourceGen/LineListGen.cs +++ b/SourceGen/LineListGen.cs @@ -443,11 +443,16 @@ namespace SourceGen { /// /// Changes the pseudo-op name object. Clears the line list, instigating a - /// full re-render. + /// full re-render. If the new set is unchanged from the old set, nothing is done. /// /// Pseudo-op names. public void SetPseudoOpNames(PseudoOp.PseudoOpNames opNames) { + if (mPseudoOpNames == opNames) { + return; + } + mPseudoOpNames = opNames; + Debug.Assert(mPseudoOpNames == opNames); mLineList.Clear(); mDisplayList.Clear(); } diff --git a/SourceGen/MainController.cs b/SourceGen/MainController.cs index d9a96f4..0336749 100644 --- a/SourceGen/MainController.cs +++ b/SourceGen/MainController.cs @@ -386,7 +386,7 @@ namespace SourceGen { /// Replaces the contents of the global settings object with the new settings, /// then applies them to the project. /// - /// + /// New settings. public void SetAppSettings(AppSettings settings) { AppSettings.Global.ReplaceSettings(settings); ApplyAppSettings(); @@ -450,6 +450,8 @@ namespace SourceGen { } + // Update the formatter, and null out mOutputFormatterCpuDef to force a refresh + // of related items. mOutputFormatter = new Formatter(mFormatterConfig); mOutputFormatterCpuDef = null; @@ -1410,6 +1412,8 @@ namespace SourceGen { EditAppSettings dlg = new EditAppSettings(owner, mMainWin, this, initialTab, initialAsmId); dlg.ShowDialog(); + + // The settings code calls SetAppSettings() directly whenever "Apply" is hit. } public void HandleCodeListDoubleClick(int row, int col) { diff --git a/SourceGen/PseudoOp.cs b/SourceGen/PseudoOp.cs index 1d23439..1d27d85 100644 --- a/SourceGen/PseudoOp.cs +++ b/SourceGen/PseudoOp.cs @@ -59,28 +59,89 @@ namespace SourceGen { } /// - /// Pseudo-op name collection. Name strings may be null. + /// Pseudo-op name collection. Instances are immutable. /// public class PseudoOpNames { - public string EquDirective { get; set; } - public string OrgDirective { get; set; } - public string RegWidthDirective { get; set; } + public string EquDirective { get; private set; } + public string OrgDirective { get; private set; } + public string RegWidthDirective { get; private set; } - public string DefineData1 { get; set; } - public string DefineData2 { get; set; } - public string DefineData3 { get; set; } - public string DefineData4 { get; set; } - public string DefineBigData2 { get; set; } - public string DefineBigData3 { get; set; } - public string DefineBigData4 { get; set; } - public string Fill { get; set; } - public string Dense { get; set; } - public string StrGeneric { get; set; } - public string StrReverse { get; set; } - public string StrLen8 { get; set; } - public string StrLen16 { get; set; } - public string StrNullTerm { get; set; } - public string StrDci { get; set; } + public string DefineData1 { get; private set; } + public string DefineData2 { get; private set; } + public string DefineData3 { get; private set; } + public string DefineData4 { get; private set; } + public string DefineBigData2 { get; private set; } + public string DefineBigData3 { get; private set; } + public string DefineBigData4 { get; private set; } + public string Fill { get; private set; } + public string Dense { get; private set; } + public string StrGeneric { get; private set; } + public string StrReverse { get; private set; } + public string StrLen8 { get; private set; } + public string StrLen16 { get; private set; } + public string StrNullTerm { get; private set; } + public string StrDci { get; private set; } + + /// + /// Constructs an empty PseudoOp. + /// + public PseudoOpNames() : this(new Dictionary()) { + } + + /// + /// Constructor. Pass in a dictionary with name/value pairs. Unknown names + /// will be ignored, missing names will be assigned the empty string. + /// + /// Dictionary of values. + public PseudoOpNames(Dictionary dict) { + foreach (PropertyInfo prop in GetType().GetProperties()) { + dict.TryGetValue(prop.Name, out string value); + if (value == null) { + value = string.Empty; + } + prop.SetValue(this, value); + } + } + + public static bool operator ==(PseudoOpNames a, PseudoOpNames b) { + if (ReferenceEquals(a, b)) { + return true; // same object, or both null + } + if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) { + return false; // one is null + } + return a.EquDirective == b.EquDirective && + a.OrgDirective == b.OrgDirective && + a.RegWidthDirective == b.RegWidthDirective && + a.DefineData1 == b.DefineData1 && + a.DefineData2 == b.DefineData2 && + a.DefineData3 == b.DefineData3 && + a.DefineData4 == b.DefineData4 && + a.DefineBigData2 == b.DefineBigData2 && + a.DefineBigData3 == b.DefineBigData3 && + a.DefineBigData4 == b.DefineBigData4 && + a.Fill == b.Fill && + a.Dense == b.Dense && + a.StrGeneric == b.StrGeneric && + a.StrReverse == b.StrReverse && + a.StrLen8 == b.StrLen8 && + a.StrLen16 == b.StrLen16 && + a.StrNullTerm == b.StrNullTerm && + a.StrDci == b.StrDci; + } + public static bool operator !=(PseudoOpNames a, PseudoOpNames b) { + return !(a == b); + } + public override bool Equals(object obj) { + return obj is PseudoOpNames && this == (PseudoOpNames)obj; + } + public override int GetHashCode() { + // should be enough + return (EquDirective == null ? 0 : EquDirective.GetHashCode()) ^ + (OrgDirective == null ? 0 : OrgDirective.GetHashCode()) ^ + (DefineData1 == null ? 0 : DefineData1.GetHashCode()) ^ + (Fill == null ? 0 : Fill.GetHashCode()); + } public string GetDefineData(int width) { switch (width) { @@ -101,11 +162,6 @@ namespace SourceGen { } } - public PseudoOpNames GetCopy() { - // Do it the lazy way. - return Deserialize(Serialize()); - } - /// /// Merges the non-null, non-empty strings in "other" into this instance. /// @@ -127,13 +183,24 @@ namespace SourceGen { // which means a lot of double-quote escaping. We could do something here // that stored more nicely but it doesn't seem worth the effort. JavaScriptSerializer ser = new JavaScriptSerializer(); - return ser.Serialize(this); + + Dictionary dict = new Dictionary(); + foreach (PropertyInfo prop in GetType().GetProperties()) { + string value = (string)prop.GetValue(this); + if (!string.IsNullOrEmpty(value)) { + dict[prop.Name] = value; + } + } + + return ser.Serialize(dict); } public static PseudoOpNames Deserialize(string cereal) { JavaScriptSerializer ser = new JavaScriptSerializer(); try { - return ser.Deserialize(cereal); + Dictionary dict = + ser.Deserialize>(cereal); + return new PseudoOpNames(dict); } catch (Exception ex) { Debug.WriteLine("PseudoOpNames deserialization failed: " + ex.Message); return new PseudoOpNames(); @@ -142,34 +209,34 @@ namespace SourceGen { } /// - /// Returns a new PseudoOpNames instance with some reasonable defaults for on-screen - /// display. + /// Returns a PseudoOpNames instance with some reasonable defaults for on-screen display. /// public static PseudoOpNames DefaultPseudoOpNames { - get { return sDefaultPseudoOpNames.GetCopy(); } + get { return sDefaultPseudoOpNames; } } - private static readonly PseudoOpNames sDefaultPseudoOpNames = new PseudoOpNames() { - EquDirective = ".eq", - OrgDirective = ".org", - RegWidthDirective = ".rwid", + private static readonly PseudoOpNames sDefaultPseudoOpNames = + new PseudoOpNames(new Dictionary { + { "EquDirective", ".eq" }, + { "OrgDirective", ".org" }, + { "RegWidthDirective", ".rwid" }, - DefineData1 = ".dd1", - DefineData2 = ".dd2", - DefineData3 = ".dd3", - DefineData4 = ".dd4", - DefineBigData2 = ".dbd2", - DefineBigData3 = ".dbd3", - DefineBigData4 = ".dbd4", - Fill = ".fill", - Dense = ".bulk", + { "DefineData1", ".dd1" }, + { "DefineData2", ".dd2" }, + { "DefineData3", ".dd3" }, + { "DefineData4", ".dd4" }, + { "DefineBigData2", ".dbd2" }, + { "DefineBigData3", ".dbd3" }, + { "DefineBigData4", ".dbd4" }, + { "Fill", ".fill" }, + { "Dense", ".bulk" }, - StrGeneric = ".str", - StrReverse = ".rstr", - StrLen8 = ".l1str", - StrLen16 = ".l2str", - StrNullTerm = ".zstr", - StrDci = ".dstr", - }; + { "StrGeneric", ".str" }, + { "StrReverse", ".rstr" }, + { "StrLen8", ".l1str" }, + { "StrLen16", ".l2str" }, + { "StrNullTerm", ".zstr" }, + { "StrDci", ".dstr" } + }); /// diff --git a/SourceGen/Tests/GenTest.cs b/SourceGen/Tests/GenTest.cs index e982a46..f9413d4 100644 --- a/SourceGen/Tests/GenTest.cs +++ b/SourceGen/Tests/GenTest.cs @@ -309,7 +309,14 @@ namespace SourceGen.Tests { ReportProgress(" verify..."); timer.StartTask("Compare Binary to Expected"); FileInfo fi = new FileInfo(asmResults.OutputPathName); - if (fi.Length != project.FileData.Length) { + if (!fi.Exists) { + // This can happen if the assembler fails to generate output but doesn't + // report an error code (e.g. Merlin 32 in certain situations). + ReportErrMsg("asm output missing"); + ReportFailure(); + didFail = true; + continue; + } else if (fi.Length != project.FileData.Length) { ReportErrMsg("asm output mismatch: length is " + fi.Length + ", expected " + project.FileData.Length); ReportFailure(); diff --git a/SourceGen/WpfGui/EditAppSettings.xaml b/SourceGen/WpfGui/EditAppSettings.xaml index 6e2cdc1..774ac34 100644 --- a/SourceGen/WpfGui/EditAppSettings.xaml +++ b/SourceGen/WpfGui/EditAppSettings.xaml @@ -629,23 +629,23 @@ limitations under the License. @@ -660,19 +660,19 @@ limitations under the License. @@ -687,19 +687,19 @@ limitations under the License. @@ -712,15 +712,15 @@ limitations under the License. diff --git a/SourceGen/WpfGui/EditAppSettings.xaml.cs b/SourceGen/WpfGui/EditAppSettings.xaml.cs index b46656e..b1ca3db 100644 --- a/SourceGen/WpfGui/EditAppSettings.xaml.cs +++ b/SourceGen/WpfGui/EditAppSettings.xaml.cs @@ -30,6 +30,7 @@ using CommonUtil; using AssemblerInfo = SourceGen.AsmGen.AssemblerInfo; using AssemblerConfig = SourceGen.AsmGen.AssemblerConfig; using ExpressionMode = Asm65.Formatter.FormatConfig.ExpressionMode; +using System.Windows.Input; namespace SourceGen.WpfGui { /// @@ -212,8 +213,16 @@ namespace SourceGen.WpfGui { string stringCereal = stringSet.Serialize(); mSettings.SetString(AppSettings.FMT_STRING_DELIM, stringCereal); - mMainCtrl.SetAppSettings(mSettings); - AsmGen.AssemblerVersionCache.QueryVersions(); + try { + // QueryVersions() can sometimes be slow under Win10 (mid 2019), possibly + // because of the built-in malware detection, so pop up a wait cursor. + Mouse.OverrideCursor = Cursors.Wait; + mMainCtrl.SetAppSettings(mSettings); + AsmGen.AssemblerVersionCache.QueryVersions(); + } finally { + Mouse.OverrideCursor = null; + } + IsDirty = false; } @@ -913,9 +922,9 @@ namespace SourceGen.WpfGui { AssemblerInfo asmInfo = (AssemblerInfo)displayFmtQuickComboBox.SelectedItem; AsmGen.IGenerator gen = AssemblerInfo.GetGenerator(asmInfo.AssemblerId); - PseudoOp.PseudoOpNames opNames; + PseudoOp.PseudoOpNames unused; Asm65.Formatter.FormatConfig formatConfig; - gen.GetDefaultDisplayFormat(out opNames, out formatConfig); + gen.GetDefaultDisplayFormat(out unused, out formatConfig); SetWidthDisamSettings(formatConfig.mForceAbsOpcodeSuffix, formatConfig.mForceLongOpcodeSuffix, @@ -983,13 +992,14 @@ namespace SourceGen.WpfGui { /// Exports values from text fields to a PseudoOpNames object. /// private PseudoOp.PseudoOpNames ExportPseudoOpNames() { - PseudoOp.PseudoOpNames opNames = new PseudoOp.PseudoOpNames(); + Dictionary dict = new Dictionary(); + for (int i = 0; i < mPseudoNameMap.Length; i++) { - // NOTE: PseudoOpNames must be a class (not a struct) or this will fail. - // SetValue() would be invoked on a boxed copy that is discarded afterward. - mPseudoNameMap[i].PropInfo.SetValue(opNames, mPseudoNameMap[i].TextBox.Text); + // Use TrimEnd() to remove invisible trailing spaces, and reduce a string + // that's nothing but blanks to empty. + dict[mPseudoNameMap[i].PropInfo.Name] = mPseudoNameMap[i].TextBox.Text.TrimEnd(); } - return opNames; + return new PseudoOp.PseudoOpNames(dict); } // Invoked when text is changed in any pseudo-op text box.