1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-11-18 15:06:07 +00:00
6502bench/SourceGen/PseudoOp.cs

946 lines
43 KiB
C#
Raw Normal View History

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