1
0
mirror of https://github.com/fadden/6502bench.git synced 2025-02-18 08:30:28 +00:00

Copy some non-UI code over

Mostly a straight copy & paste of the files.  The only significant
change was to move the localizable strings from Properties/Resources
(RESX) to Res/Strings.xaml (Resource Dictionary).  I expect a
number of strings will no longer be needed, since WPF lets you put
more of the UI/UX logic into the design side.

I also renamed the namespace to SourceGenWPF, and put the app icon
into the Res directory so it can be a resource rather than a loose
file.  I'm merging the "Setup" directory contents into the main app
since there wasn't a whole lot going on there.

The WPF Color class lacks conversions to/from a 32-bit integer, so
I added those.

None of the stuff is wired up yet.
This commit is contained in:
Andy McFadden 2019-05-02 15:45:40 -07:00
parent 8ceae370cc
commit 575f834b1d
38 changed files with 12564 additions and 2 deletions

296
SourceGenWPF/AddressMap.cs Normal file
View File

@ -0,0 +1,296 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
namespace SourceGenWPF {
/// <summary>
/// Map file offsets to 65xx addresses and vice-versa. Useful for sources with
/// multiple ORG directives.
///
/// It's possible to generate code that would overlap once relocated at run time,
/// which means a given address could map to multiple offsets. For this reason
/// it's useful to know the offset of the referring code when evaluating a
/// reference, so that a "local" match can take priority.
/// </summary>
public class AddressMap : IEnumerable<AddressMap.AddressMapEntry> {
/// <summary>
/// Code starting at the specified offset will have the specified address.
///
/// The entries are held in the list in order, sorted by offset, with no gaps.
/// This makes the "length" field redundant, as it can be computed by
/// (entry[N+1].mOffset - entry[N].mOffset), with a special case for the last
/// entry in the list. It's convenient to maintain it explicitly however, as
/// the list is read far more often than it is updated.
///
/// Entries are mutable, but must only be altered by AddressMap. Don't retain
/// instances of this across other activity.
/// </summary>
public class AddressMapEntry {
public int Offset { get; set; }
public int Addr { get; set; }
public int Length { get; set; }
public AddressMapEntry(int offset, int addr, int len) {
Offset = offset;
Addr = addr;
Length = len;
}
}
/// <summary>
/// Total length, in bytes, spanned by this map.
/// </summary>
private int mTotalLength;
/// <summary>
/// List of definitions, in sorted order.
/// </summary>
private List<AddressMapEntry> mAddrList = new List<AddressMapEntry>();
/// <summary>
/// Constructor.
/// </summary>
/// <param name="length">Total length, in bytes, spanned by this map.</param>
public AddressMap(int length) {
/// There must always be at least one entry, defining the target address
/// for file offset 0. This can be changed, but can't be removed.
mTotalLength = length;
mAddrList.Add(new AddressMapEntry(0, 0, length));
}
// IEnumerable
public IEnumerator<AddressMapEntry> GetEnumerator() {
return ((IEnumerable<AddressMapEntry>)mAddrList).GetEnumerator();
}
// IEnumerable
IEnumerator IEnumerable.GetEnumerator() {
return ((IEnumerable<AddressMapEntry>)mAddrList).GetEnumerator();
}
/// <summary>
/// Returns the Nth entry in the address map.
/// </summary>
public AddressMapEntry this[int i] {
get { return mAddrList[i]; }
}
/// <summary>
/// Number of entries in the address map.
/// </summary>
public int Count { get { return mAddrList.Count; } }
/// <summary>
/// Returns the Address value of the address map entry associated with the specified
/// offset, or -1 if there is no address map entry there. The offset must match exactly.
/// </summary>
public int Get(int offset) {
foreach (AddressMapEntry ad in mAddrList) {
if (ad.Offset == offset) {
return ad.Addr;
}
}
return -1;
}
/// <summary>
/// Returns the index of the address map entry that contains the given offset.
/// We assume the offset is valid.
/// </summary>
private int IndexForOffset(int offset) {
for (int i = 1; i < mAddrList.Count; i++) {
if (mAddrList[i].Offset > offset) {
return i - 1;
}
}
return mAddrList.Count - 1;
}
/// <summary>
/// Adds, updates, or removes a map entry.
/// </summary>
/// <param name="offset">File offset at which the address changes.</param>
/// <param name="addr">24-bit address.</param>
public void Set(int offset, int addr) {
Debug.Assert(offset >= 0);
if (addr == -1) {
if (offset != 0) { // ignore attempts to remove entry at offset zero
Remove(offset);
}
return;
}
Debug.Assert(addr >= 0 && addr < 0x01000000); // 24-bit address space
int i;
for (i = 0; i < mAddrList.Count; i++) {
AddressMapEntry ad = mAddrList[i];
if (ad.Offset == offset) {
// update existing
ad.Addr = addr;
mAddrList[i] = ad;
return;
} else if (ad.Offset > offset) {
// The i'th entry is one past the interesting part.
break;
}
}
// Carve a chunk out of the previous entry.
AddressMapEntry prev = mAddrList[i - 1];
int prevOldLen = prev.Length;
int prevNewLen = offset - prev.Offset;
prev.Length = prevNewLen;
mAddrList[i - 1] = prev;
mAddrList.Insert(i,
new AddressMapEntry(offset, addr, prevOldLen - prevNewLen));
DebugValidate();
}
/// <summary>
/// Removes an entry from the set.
/// </summary>
/// <param name="offset">The initial offset of the mapping to remove. This
/// must be the initial value, not a mid-range value.</param>
/// <returns>True if something was removed.</returns>
public bool Remove(int offset) {
if (offset == 0) {
throw new Exception("Not allowed to remove entry 0");
}
for (int i = 1; i < mAddrList.Count; i++) {
if (mAddrList[i].Offset == offset) {
// Add the length to the previous entry.
AddressMapEntry prev = mAddrList[i - 1];
prev.Length += mAddrList[i].Length;
mAddrList[i - 1] = prev;
mAddrList.RemoveAt(i);
DebugValidate();
return true;
}
}
return false;
}
/// <summary>
/// Returns true if the given address falls into the range spanned by the
/// address map entry.
/// </summary>
/// <param name="index">Address map entry index.</param>
/// <param name="addr">Address to check.</param>
/// <returns></returns>
private bool IndexContainsAddress(int index, int addr) {
return addr >= mAddrList[index].Addr &&
addr < mAddrList[index].Addr + mAddrList[index].Length;
}
/// <summary>
/// Determines the file offset that best contains the specified target address.
/// </summary>
/// <param name="srcOffset">Offset of the address reference.</param>
/// <param name="targetAddr">Address to look up.</param>
/// <returns>The file offset, or -1 if the address falls outside the file.</returns>
public int AddressToOffset(int srcOffset, int targetAddr) {
if (mAddrList.Count == 1) {
// Trivial case.
if (IndexContainsAddress(0, targetAddr)) {
Debug.Assert(targetAddr >= mAddrList[0].Addr);
return targetAddr - mAddrList[0].Addr;
} else {
return -1;
}
}
// We have multiple, potentially overlapping address ranges. Start by
// looking for a match in the srcOffset range; if that fails, scan
// forward from the start.
int srcOffIndex = IndexForOffset(srcOffset);
if (IndexContainsAddress(srcOffIndex, targetAddr)) {
Debug.Assert(targetAddr >= mAddrList[srcOffIndex].Addr);
return (targetAddr - mAddrList[srcOffIndex].Addr) + mAddrList[srcOffIndex].Offset;
}
for (int i = 0; i < mAddrList.Count; i++) {
if (i == srcOffIndex) {
// optimization -- we already checked this one
continue;
}
if (IndexContainsAddress(i, targetAddr)) {
Debug.Assert(targetAddr >= mAddrList[i].Addr);
return (targetAddr - mAddrList[i].Addr) + mAddrList[i].Offset;
}
}
return -1;
}
/// <summary>
/// Converts a file offset to an address.
/// </summary>
/// <param name="offset">File offset.</param>
/// <returns>24-bit address.</returns>
public int OffsetToAddress(int offset) {
int srcOffIndex = IndexForOffset(offset);
return mAddrList[srcOffIndex].Addr + (offset - mAddrList[srcOffIndex].Offset);
}
/// <summary>
/// Internal consistency checks.
/// </summary>
private void DebugValidate() {
if (mAddrList.Count < 1) {
throw new Exception("AddressMap: empty");
}
if (mAddrList[0].Offset != 0) {
throw new Exception("AddressMap: bad offset 0");
}
if (mAddrList.Count == 1) {
if (mAddrList[0].Length != mTotalLength) {
throw new Exception("AddressMap: single entry len bad");
}
} else {
int totalLen = 0;
for (int i = 0; i < mAddrList.Count; i++) {
AddressMapEntry ent = mAddrList[i];
if (i != 0) {
if (ent.Offset != mAddrList[i - 1].Offset + mAddrList[i - 1].Length) {
throw new Exception("Bad offset step to " + i);
}
}
totalLen += ent.Length;
}
if (totalLen != mTotalLength) {
throw new Exception("AddressMap: bad length sum (" + totalLen + " vs " +
mTotalLength + ")");
}
}
}
public override string ToString() {
return "[AddressMap: " + mAddrList.Count + " entries]";
}
}
}

355
SourceGenWPF/Anattrib.cs Normal file
View File

@ -0,0 +1,355 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Diagnostics;
using System.Text;
using Asm65;
namespace SourceGenWPF {
/// <summary>
/// Analyzer attribute holder. Contains the output of the instruction and data analyzers.
/// Every byte in the input file has one of these associated with it.
///
/// (Yes, it's a mutable struct. Yes, that fact has bitten me a few times. The array
/// of these may have millions of elements, so the reduction in overhead seems worthwhile.)
/// </summary>
public struct Anattrib {
[FlagsAttribute]
private enum AttribFlags {
InstrStart = 1 << 0, // byte is first of an instruction
Instruction = 1 << 1, // byte is part of an instruction or inline data
InlineData = 1 << 2, // byte is inline data
Data = 1 << 3, // byte is data
EntryPoint = 1 << 8, // external code branches here
BranchTarget = 1 << 9, // internal code branches here
ExternalBranch = 1 << 10, // this abs/rel branch lands outside input file
NoContinue = 1 << 12, // execution does not continue to following instruction
Visited = 1 << 16, // has the analyzer visited this byte?
Changed = 1 << 17, // set/cleared as the analyzer works
Hinted = 1 << 18, // was this byte affected by a type hint?
}
// Flags indicating what type of data is here. Use the following Is* properties
// to set/clear.
private AttribFlags mAttribFlags;
public bool IsInstructionStart {
get {
return (mAttribFlags & AttribFlags.InstrStart) != 0;
}
set {
IsInstruction = value;
if (value) {
mAttribFlags |= AttribFlags.InstrStart;
} else {
mAttribFlags &= ~AttribFlags.InstrStart;
}
}
}
public bool IsInstruction {
get {
return (mAttribFlags & AttribFlags.Instruction) != 0;
}
set {
Debug.Assert(value == false ||
(mAttribFlags & (AttribFlags.InlineData | AttribFlags.Data)) == 0);
if (value) {
mAttribFlags |= AttribFlags.Instruction;
} else {
mAttribFlags &= ~AttribFlags.Instruction;
}
}
}
public bool IsInlineData {
get {
return (mAttribFlags & AttribFlags.InlineData) != 0;
}
set {
Debug.Assert(value == false ||
(mAttribFlags & (AttribFlags.Instruction | AttribFlags.Data)) == 0);
if (value) {
mAttribFlags |= AttribFlags.InlineData;
} else {
mAttribFlags &= ~AttribFlags.InlineData;
}
}
}
public bool IsData {
get {
return (mAttribFlags & AttribFlags.Data) != 0;
}
set {
Debug.Assert(value == false ||
(mAttribFlags & (AttribFlags.InlineData | AttribFlags.Instruction)) == 0);
if (value) {
mAttribFlags |= AttribFlags.Data;
} else {
mAttribFlags &= ~AttribFlags.Data;
}
}
}
public bool IsStart {
get {
return IsInstructionStart || IsDataStart || IsInlineDataStart;
}
}
public bool IsEntryPoint {
get {
return (mAttribFlags & AttribFlags.EntryPoint) != 0;
}
set {
if (value) {
mAttribFlags |= AttribFlags.EntryPoint;
} else {
mAttribFlags &= ~AttribFlags.EntryPoint;
}
}
}
public bool IsBranchTarget {
get {
return (mAttribFlags & AttribFlags.BranchTarget) != 0;
}
set {
if (value) {
mAttribFlags |= AttribFlags.BranchTarget;
} else {
mAttribFlags &= ~AttribFlags.BranchTarget;
}
}
}
public bool IsExternalBranch {
get {
return (mAttribFlags & AttribFlags.ExternalBranch) != 0;
}
set {
if (value) {
mAttribFlags |= AttribFlags.ExternalBranch;
} else {
mAttribFlags &= ~AttribFlags.ExternalBranch;
}
}
}
public bool DoesNotContinue {
get {
return (mAttribFlags & AttribFlags.NoContinue) != 0;
}
set {
if (value) {
mAttribFlags |= AttribFlags.NoContinue;
} else {
mAttribFlags &= ~AttribFlags.NoContinue;
}
}
}
public bool DoesNotBranch {
get {
return (BranchTaken == OpDef.BranchTaken.Never);
}
}
public bool IsVisited {
get {
return (mAttribFlags & AttribFlags.Visited) != 0;
}
set {
if (value) {
mAttribFlags |= AttribFlags.Visited;
} else {
mAttribFlags &= ~AttribFlags.Visited;
}
}
}
public bool IsChanged {
get {
return (mAttribFlags & AttribFlags.Changed) != 0;
}
set {
if (value) {
mAttribFlags |= AttribFlags.Changed;
} else {
mAttribFlags &= ~AttribFlags.Changed;
}
}
}
public bool IsHinted {
get {
return (mAttribFlags & AttribFlags.Hinted) != 0;
}
set {
if (value) {
mAttribFlags |= AttribFlags.Hinted;
} else {
mAttribFlags &= ~AttribFlags.Hinted;
}
}
}
public bool IsDataStart {
get {
return IsData && DataDescriptor != null;
}
}
public bool IsInlineDataStart {
get {
return IsInlineData && DataDescriptor != null;
}
}
/// <summary>
/// Get the target memory address for this byte.
/// </summary>
public int Address { get; set; }
/// <summary>
/// Instructions: length of the instruction (for InstrStart). If a FormatDescriptor
/// is assigned, the length must match.
/// Inline data: FormatDescriptor length, or zero if no descriptor is defined.
/// Data: FormatDescriptor length, or zero if no descriptor is defined.
///
/// This field should only be set by CodeAnalysis methods, although the "get" value
/// can be changed for data/inline-data by setting the DataDescriptor field.
/// </summary>
public int Length {
get {
// For data we don't even use the field; this ensures that we're always
// using the FormatDescriptor's length.
if (IsData || IsInlineData) {
Debug.Assert(mLength == 0);
if (DataDescriptor != null) {
return DataDescriptor.Length;
} else {
return 0;
}
}
return mLength;
}
set {
Debug.Assert(!IsData);
mLength = value;
}
}
private int mLength;
/// <summary>
/// Instructions only: processor status flags.
///
/// Note this returns a copy of a struct, so modifications to the returned value
/// (including calls to Merge and Apply) are not permanent.
/// </summary>
public StatusFlags StatusFlags {
get { return mStatusFlags; }
set { mStatusFlags = value; }
}
private StatusFlags mStatusFlags;
public void MergeStatusFlags(StatusFlags other) {
mStatusFlags.Merge(other);
}
public void ApplyStatusFlags(StatusFlags other) {
mStatusFlags.Apply(other);
}
/// <summary>
/// Branch instructions only: outcome of branch.
/// </summary>
public OpDef.BranchTaken BranchTaken { get; set; }
/// <summary>
/// Instructions only: decoded operand address value. Will be -1 if not
/// yet computed or not applicable. For a relative branch instruction,
/// this will have the absolute branch target address. On the 65816, this
/// will be a 24-bit address.
/// </summary>
public int OperandAddress {
get { return mOperandAddressSet ? mOperandAddress : -1; }
set {
Debug.Assert(mOperandAddress >= -1);
mOperandAddress = value;
mOperandAddressSet = (value >= 0);
}
}
private int mOperandAddress;
private bool mOperandAddressSet;
/// <summary>
/// Instructions only: offset referenced by OperandAddress. Will be -1 if not
/// yet computed, not applicable, or if OperandAddress refers to a location
/// outside the scope of the file.
/// </summary>
public int OperandOffset {
get { return mOperandOffsetSet ? mOperandOffset : -1; }
set {
Debug.Assert(mOperandOffset >= -1);
mOperandOffset = value;
mOperandOffsetSet = (value >= 0);
}
}
private int mOperandOffset;
private bool mOperandOffsetSet;
/// <summary>
/// Instructions only: is OperandOffset a direct target offset? (This is used when
/// tracing jump instructions, to know if we should add the offset to the scan list.
/// It's determined by the opcode, e.g. "JMP addr" -> true, "JMP (addr,X)" -> false.)
/// </summary>
public bool IsOperandOffsetDirect { get; set; }
/// <summary>
/// Symbol defined as the label for this offset. All offsets that are instruction
/// or data target offsets will have one of these defined. Users can define additional
/// symbols as well.
///
/// Will be null if no label is defined for this offset.
/// </summary>
public Symbol Symbol { get; set; }
/// <summary>
/// Format descriptor for operands and data items. Will be null if no descriptor
/// is defined for this offset.
/// </summary>
public FormatDescriptor DataDescriptor { get; set; }
/// <summary>
/// Is this an instruction with an operand (i.e. not impl/acc)?
/// </summary>
public bool IsInstructionWithOperand {
get {
if (!IsInstructionStart) {
return false;
}
return Length != 1;
}
}
/// <summary>
/// Returns a fixed-width string with indicators for items of interest.
/// </summary>
public string ToAttrString() {
StringBuilder sb = new StringBuilder(5);
char blank = '.';
sb.Append(IsEntryPoint ? '@' : blank);
sb.Append(IsHinted ? 'H' : blank);
sb.Append(DoesNotBranch ? '!' : blank);
sb.Append(DoesNotContinue ? '#' : blank);
sb.Append(IsBranchTarget ? '>' : blank);
return sb.ToString();
}
}
}

View File

@ -20,5 +20,11 @@ limitations under the License.
StartupUri="ProjWin/MainWindow.xaml">
<Application.Resources>
<FontFamily x:Key="GeneralMonoFont">Consolas</FontFamily>
<ResourceDictionary x:Key="whatever">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Res/Strings.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

355
SourceGenWPF/AppSettings.cs Normal file
View File

@ -0,0 +1,355 @@
/*
* 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.Text;
using System.Web.Script.Serialization;
namespace SourceGenWPF {
/// <summary>
/// Application settings registry. This holds both user-accessible settings and saved
/// values like window widths.
///
/// Everything is stored as name/value pairs, where the value is serialized as a string.
/// Names are case-sensitive.
///
/// We don't discard things we don't recognize. If we somehow end up reading a config
/// file from a newer version of the app, the various settings will be retained.
/// </summary>
public class AppSettings {
#region Names
// Name constants. Having them defined here avoids collisions and misspellings, and
// makes it easy to find all uses.
// Main window.
public const string MAIN_WINDOW_WIDTH = "main-window-width";
public const string MAIN_WINDOW_HEIGHT = "main-window-height";
public const string MAIN_WINDOW_LOC_X = "main-window-loc-x";
public const string MAIN_WINDOW_LOC_Y = "main-window-loc-y";
public const string MAIN_WINDOW_MAXIMIZED = "main-window-maximized";
public const string MAIN_LEFT_PANEL_WIDTH = "main-left-panel-width";
public const string MAIN_RIGHT_PANEL_WIDTH = "main-right-panel-width";
public const string MAIN_LEFT_SIDE_SPLITTER_DIST = "main-left-side-splitter-dist";
public const string MAIN_RIGHT_SIDE_SPLITTER_DIST = "main-right-side-splitter-dist";
// New project dialog.
public const string NEWP_SELECTED_SYSTEM = "newp-selected-system";
// Formatting choices.
public const string FMT_UPPER_HEX_DIGITS = "fmt-upper-hex-digits";
public const string FMT_UPPER_OP_MNEMONIC = "fmt-upper-op-mnemonic";
public const string FMT_UPPER_PSEUDO_OP_MNEMONIC = "fmt-upper-pseudo-op-mnemonic";
public const string FMT_UPPER_OPERAND_A = "fmt-upper-operand-a";
public const string FMT_UPPER_OPERAND_S = "fmt-upper-operand-s";
public const string FMT_UPPER_OPERAND_XY = "fmt-upper-operand-xy";
public const string FMT_ADD_SPACE_FULL_COMMENT = "fmt-add-space-full-comment";
public const string FMT_SPACES_BETWEEN_BYTES = "fmt-spaces-between-bytes";
public const string FMT_OPCODE_SUFFIX_ABS = "fmt-opcode-suffix-abs";
public const string FMT_OPCODE_SUFFIX_LONG = "fmt-opcode-suffix-long";
public const string FMT_OPERAND_PREFIX_ABS = "fmt-operand-prefix-abs";
public const string FMT_OPERAND_PREFIX_LONG = "fmt-operand-prefix-long";
public const string FMT_EXPRESSION_MODE = "fmt-expression-mode";
public const string FMT_PSEUDO_OP_NAMES = "fmt-pseudo-op-names";
public const string CLIP_LINE_FORMAT = "clip-line-format";
// Symbol-list window options.
public const string SYMWIN_SHOW_USER = "symwin-show-user";
public const string SYMWIN_SHOW_AUTO = "symwin-show-auto";
public const string SYMWIN_SHOW_PROJECT = "symwin-show-project";
public const string SYMWIN_SHOW_PLATFORM = "symwin-show-platform";
public const string SYMWIN_SHOW_CONST = "symwin-show-const";
public const string SYMWIN_SHOW_ADDR = "symwin-show-addr";
public const string SYMWIN_SORT_ASCENDING = "symwin-sort-ascending";
public const string SYMWIN_SORT_COL = "symwin-sort-col";
public const string SYMWIN_COL_WIDTHS = "symwin-col-widths";
// References window options.
public const string REFWIN_COL_WIDTHS = "refwin-col-widths";
// Notes window options.
public const string NOTEWIN_COL_WIDTHS = "notewin-col-widths";
// Code List View settings.
public const string CDLV_COL_WIDTHS = "cdlv-col-widths";
public const string CDLV_FONT = "cdlv-font";
// Hex dump viewer settings.
public const string HEXD_ASCII_ONLY = "hexd-ascii-only";
public const string HEXD_CHAR_CONV = "hexd-char-conv";
// ASCII chart viewer settings.
public const string ASCCH_MODE = "ascch-mode";
// Source generation settings.
public const string SRCGEN_DEFAULT_ASM = "srcgen-default-asm";
public const string SRCGEN_ADD_IDENT_COMMENT = "srcgen-add-ident-comment";
public const string SRCGEN_DISABLE_LABEL_LOCALIZATION = "srcgen-disable-label-localization";
public const string SRCGEN_LONG_LABEL_NEW_LINE = "srcgen-long-label-new-line";
public const string SRCGEN_SHOW_CYCLE_COUNTS = "srcgen-show-cycle-counts";
// Main project view settings.
public const string PRVW_RECENT_PROJECT_LIST = "prvw-recent-project-list";
// Assembler settings prefix
public const string ASM_CONFIG_PREFIX = "asm-config-";
// Internal debugging features.
public const string DEBUG_MENU_ENABLED = "debug-menu-enabled";
#endregion Names
#region Implementation
// App settings file header.
public const string MAGIC = "### 6502bench SourceGen settings v1.0 ###";
/// <summary>
/// Single global instance of app settings.
/// </summary>
public static AppSettings Global {
get {
return sSingleton;
}
}
private static AppSettings sSingleton = new AppSettings();
/// <summary>
/// Dirty flag, set to true by every "set" call.
/// </summary>
public bool Dirty { get; set; }
/// <summary>
/// Settings storage.
/// </summary>
private Dictionary<string, string> mSettings = new Dictionary<string, string>();
private AppSettings() { }
/// <summary>
/// Creates a copy of this object.
/// </summary>
/// <returns></returns>
public AppSettings GetCopy() {
AppSettings copy = new AppSettings();
//copy.mSettings.EnsureCapacity(mSettings.Count);
foreach (KeyValuePair<string, string> kvp in mSettings) {
copy.mSettings.Add(kvp.Key, kvp.Value);
}
return copy;
}
/// <summary>
/// Replaces the existing list of settings with a new list.
///
/// This can be used to replace the contents of the global settings object without
/// discarding the object itself, which is useful in case something has cached a
/// reference to the singleton.
/// </summary>
/// <param name="newSettings"></param>
public void ReplaceSettings(AppSettings newSettings) {
// Clone the new list, and stuff it into the old object. This way the
// objects aren't sharing lists.
mSettings = newSettings.GetCopy().mSettings;
Dirty = true;
}
/// <summary>
/// Merges settings from another settings object into this one.
/// </summary>
/// <param name="settings"></param>
/// <param name="newSettings"></param>
public void MergeSettings(AppSettings newSettings) {
foreach (KeyValuePair<string, string> kvp in newSettings.mSettings) {
mSettings[kvp.Key] = kvp.Value;
}
Dirty = true;
}
/// <summary>
/// Retrieves an integer setting.
/// </summary>
/// <param name="name">Setting name.</param>
/// <param name="defaultValue">Setting default value.</param>
/// <returns>The value found, or the default value if no setting with the specified
/// name exists, or the stored value is not an integer.</returns>
public int GetInt(string name, int defaultValue) {
if (!mSettings.TryGetValue(name, out string valueStr)) {
return defaultValue;
}
if (!int.TryParse(valueStr, out int value)) {
Debug.WriteLine("Warning: int parse failed on " + name + "=" + valueStr);
return defaultValue;
}
return value;
}
/// <summary>
/// Sets an integer setting.
/// </summary>
/// <param name="name">Setting name.</param>
/// <param name="value">Setting value.</param>
public void SetInt(string name, int value) {
mSettings[name] = value.ToString();
Dirty = true;
}
/// <summary>
/// Retrieves a boolean setting.
/// </summary>
/// <param name="name">Setting name.</param>
/// <param name="defaultValue">Setting default value.</param>
/// <returns>The value found, or the default value if no setting with the specified
/// name exists, or the stored value is not a boolean.</returns>
public bool GetBool(string name, bool defaultValue) {
if (!mSettings.TryGetValue(name, out string valueStr)) {
return defaultValue;
}
if (!bool.TryParse(valueStr, out bool value)) {
Debug.WriteLine("Warning: bool parse failed on " + name + "=" + valueStr);
return defaultValue;
}
return value;
}
/// <summary>
/// Sets a boolean setting.
/// </summary>
/// <param name="name">Setting name.</param>
/// <param name="value">Setting value.</param>
public void SetBool(string name, bool value) {
mSettings[name] = value.ToString();
Dirty = true;
}
/// <summary>
/// Retrieves an enumerated value setting.
/// </summary>
/// <param name="name">Setting name.</param>
/// <param name="enumType">Enum type that the value is part of.</param>
/// <param name="defaultValue">Setting default value.</param>
/// <returns>The value found, or the default value if no setting with the specified
/// name exists, or the stored value is not a member of the specified enumerated
/// type.</returns>
public int GetEnum(string name, Type enumType, int defaultValue) {
if (!mSettings.TryGetValue(name, out string valueStr)) {
return defaultValue;
}
try {
object o = Enum.Parse(enumType, valueStr);
return (int)o;
} catch (ArgumentException ae) {
Debug.WriteLine("Failed to parse " + valueStr + " (enum " + enumType + "): " +
ae.Message);
return defaultValue;
}
}
/// <summary>
/// Sets an enumerated setting.
/// </summary>
/// <param name="name">Setting name.</param>
/// <param name="enumType">Enum type.</param>
/// <param name="value">Setting value (integer enum index).</param>
public void SetEnum(string name, Type enumType, int value) {
mSettings[name] = Enum.GetName(enumType, value);
Dirty = true;
}
/// <summary>
/// Retrieves a string setting. The default value will be returned if the key
/// is not found, or if the value is null.
/// </summary>
/// <param name="name">Setting name.</param>
/// <param name="defaultValue">Setting default value.</param>
/// <returns>The value found, or defaultValue if not value is found.</returns>
public string GetString(string name, string defaultValue) {
if (!mSettings.TryGetValue(name, out string valueStr) || valueStr == null) {
return defaultValue;
}
return valueStr;
}
/// <summary>
/// Sets a string setting.
/// </summary>
/// <param name="name">Setting name.</param>
/// <param name="value">Setting value.</param>
public void SetString(string name, string value) {
if (value == null) {
mSettings.Remove(name);
} else {
mSettings[name] = value;
}
Dirty = true;
}
/// <summary>
/// Serializes settings dictionary into a string, for saving settings to a file.
/// </summary>
/// <returns>Serialized settings.</returns>
public string Serialize() {
StringBuilder sb = new StringBuilder(1024);
sb.Append(MAGIC); // augment with version string, which will be stripped
sb.Append("\r\n"); // will be ignored by deserializer; might get converted to \n
JavaScriptSerializer ser = new JavaScriptSerializer();
string cereal = ser.Serialize(mSettings);
// add some linefeeds to make it easier for humans
cereal = CommonUtil.TextUtil.NonQuoteReplace(cereal, ",\"", ",\r\n\"");
sb.Append(cereal);
// Stick a linefeed at the end.
sb.Append("\r\n");
return sb.ToString();
}
/// <summary>
/// Deserializes settings from a string, for loading settings from a file.
/// </summary>
/// <param name="cereal">Serialized settings.</param>
/// <returns>Deserialized settings, or null if deserialization failed.</returns>
public static AppSettings Deserialize(string cereal) {
if (!cereal.StartsWith(MAGIC)) {
return null;
}
// Skip past header.
cereal = cereal.Substring(MAGIC.Length);
AppSettings settings = new AppSettings();
JavaScriptSerializer ser = new JavaScriptSerializer();
try {
settings.mSettings = ser.Deserialize<Dictionary<string, string>>(cereal);
return settings;
} catch (Exception ex) {
Debug.WriteLine("Settings deserialization failed: " + ex.Message);
return null;
}
}
#endregion Implementation
}
}

164
SourceGenWPF/AutoLabel.cs Normal file
View File

@ -0,0 +1,164 @@
/*
* 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.Text;
using System.Diagnostics;
namespace SourceGenWPF {
/// <summary>
/// Functions for generation of "auto" labels.
/// </summary>
public static class AutoLabel {
/// <summary>
/// Auto-label style enumeration. Values were chosen to map directly to a combo box.
/// </summary>
public enum Style {
Unknown = -1,
Simple = 0,
Annotated = 1,
FullyAnnotated = 2
}
/// <summary>
/// Generates a unique address symbol. Does not add the symbol to the table.
///
/// This does not follow any Formatter rules -- labels are always entirely upper-case.
/// </summary>
/// <param name="addr">Address that label will be applied to.</param>
/// <param name="symbols">Symbol table, for uniqueness check.</param>
/// <param name="prefix">Prefix to use; must start with a letter.</param>
/// <returns>Newly-created, unique symbol.</returns>
public static Symbol GenerateUniqueForAddress(int addr, SymbolTable symbols,
string prefix) {
// $1234 == L1234, $05/1234 == L51234.
string label = prefix + addr.ToString("X4"); // always upper-case
if (symbols.TryGetValue(label, out Symbol unused)) {
const int MAX_RENAME = 999;
string baseLabel = label;
StringBuilder sb = new StringBuilder(baseLabel.Length + 8);
int index = -1;
do {
// This is expected to be unlikely and infrequent, so a simple linear
// probe for uniqueness is fine. Labels are based on the address, not
// the offset, so even without user-created labels there's still an
// opportunity for duplication.
index++;
sb.Clear();
sb.Append(baseLabel);
sb.Append('_');
sb.Append(index);
label = sb.ToString();
} while (index <= MAX_RENAME && symbols.TryGetValue(label, out unused));
if (index > MAX_RENAME) {
// I give up
throw new Exception("Too many identical symbols: " + label);
}
}
Symbol sym = new Symbol(label, addr, Symbol.Source.Auto,
Symbol.Type.LocalOrGlobalAddr);
return sym;
}
/// <summary>
/// Source reference type.
///
/// The enum is in priority order, i.e. the lowest-valued item "wins" in situations
/// where only one value is used.
/// </summary>
[Flags]
private enum RefTypes {
None = 0,
SubCall = 1 << 0,
Branch = 1 << 1,
DataRef = 1 << 2,
Write = 1 << 3,
Read = 1 << 4,
}
private static readonly char[] TAGS = { 'S', 'B', 'D', 'W', 'R' };
/// <summary>
/// Generates an auto-label with a prefix string based on the XrefSet.
/// </summary>
/// <param name="addr">Address that label will be applied to.</param>
/// <param name="symbols">Symbol table, for uniqueness check.</param>
/// <param name="xset">Cross-references for this location.</param>
/// <returns>Newly-created, unique symbol.</returns>
public static Symbol GenerateAnnotatedLabel(int addr, SymbolTable symbols,
XrefSet xset, Style style) {
Debug.Assert(xset != null);
Debug.Assert(style != Style.Simple);
RefTypes rtypes = RefTypes.None;
foreach (XrefSet.Xref xr in xset) {
switch (xr.Type) {
case XrefSet.XrefType.SubCallOp:
rtypes |= RefTypes.SubCall;
break;
case XrefSet.XrefType.BranchOp:
rtypes |= RefTypes.Branch;
break;
case XrefSet.XrefType.RefFromData:
rtypes |= RefTypes.DataRef;
break;
case XrefSet.XrefType.MemAccessOp:
switch (xr.AccType) {
case Asm65.OpDef.MemoryEffect.Read:
rtypes |= RefTypes.Read;
break;
case Asm65.OpDef.MemoryEffect.Write:
rtypes |= RefTypes.Write;
break;
case Asm65.OpDef.MemoryEffect.ReadModifyWrite:
rtypes |= RefTypes.Read;
rtypes |= RefTypes.Write;
break;
case Asm65.OpDef.MemoryEffect.None:
case Asm65.OpDef.MemoryEffect.Unknown:
break;
default:
Debug.Assert(false);
break;
}
break;
default:
Debug.Assert(false);
break;
}
}
if (rtypes == RefTypes.None) {
// unexpected
Debug.Assert(false);
return GenerateUniqueForAddress(addr, symbols, "X_");
}
StringBuilder sb = new StringBuilder(8);
for (int i = 0; i < TAGS.Length; i++) {
if (((int) rtypes & (1 << i)) != 0) {
sb.Append(TAGS[i]);
if (style == Style.Annotated) {
break;
}
}
}
sb.Append('_');
return GenerateUniqueForAddress(addr, symbols, sb.ToString());
}
}
}

109
SourceGenWPF/ChangeSet.cs Normal file
View File

@ -0,0 +1,109 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
namespace SourceGenWPF {
/// <summary>
/// Holds information about a set of changes.
///
/// Does not have hooks into other data structures. This just holds the information
/// about the changes.
/// </summary>
public class ChangeSet : IEnumerable<UndoableChange> {
private List<UndoableChange> mChanges;
/// <summary>
/// Constructs an empty ChangeSet with the specified initial capacity.
/// </summary>
/// <param name="capacity">Initial number of elements that the set can contain.</param>
public ChangeSet(int capacity) {
mChanges = new List<UndoableChange>(capacity);
}
/// <summary>
/// Constructs a ChangeSet with a single change.
/// </summary>
public ChangeSet(UndoableChange ac) {
mChanges = new List<UndoableChange>(1);
mChanges.Add(ac);
}
/// <summary>
/// The number of changes in the set.
/// </summary>
public int Count { get { return mChanges.Count; } }
/// <summary>
/// Returns the Nth change in the set.
/// </summary>
/// <param name="key">Change index.</param>
public UndoableChange this[int key] {
get {
return mChanges[key];
}
}
/// <summary>
/// Adds a change to the change set.
/// </summary>
/// <param name="change">Change to add.</param>
public void Add(UndoableChange change) {
Debug.Assert(change != null);
mChanges.Add(change);
}
/// <summary>
/// Adds a change to the change set if the object is non-null.
/// </summary>
/// <param name="change">Change to add, or null.</param>
public void AddNonNull(UndoableChange change) {
if (change != null) {
Add(change);
}
}
/// <summary>
/// Trims unused capacity from the set.
/// </summary>
public void TrimExcess() {
mChanges.TrimExcess();
}
// IEnumerable, so we can use foreach syntax when going forward
public IEnumerator GetEnumerator() {
return mChanges.GetEnumerator();
}
// IEnumerable: generic version
IEnumerator<UndoableChange> IEnumerable<UndoableChange>.GetEnumerator() {
return mChanges.GetEnumerator();
}
// TODO(maybe): reverse-order enumerator?
public override string ToString() {
string str = "[CS: count=" + mChanges.Count;
if (mChanges.Count > 0) {
str += " {0:" + mChanges[0] + "}";
}
str += "]";
return str;
}
}
}

1070
SourceGenWPF/CodeAnalysis.cs Normal file

File diff suppressed because it is too large Load Diff

1139
SourceGenWPF/DataAnalysis.cs Normal file

File diff suppressed because it is too large Load Diff

104
SourceGenWPF/DefSymbol.cs Normal file
View File

@ -0,0 +1,104 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace SourceGenWPF {
/// <summary>
/// Subclass of Symbol used for symbols defined in the platform or project.
///
/// Instances are immutable.
/// </summary>
public class DefSymbol : Symbol {
/// <summary>
/// Data format descriptor.
/// </summary>
public FormatDescriptor DataDescriptor { get; private set; }
/// <summary>
/// User-supplied comment.
/// </summary>
public string Comment { get; private set; }
public string Tag { get; private set; }
/// <summary>
/// Cross-reference data, generated by the analyzer.
/// </summary>
public XrefSet Xrefs { get; private set; }
// NOTE: might be nice to identify the symbol's origin, e.g. which platform
// symbol file it was defined in. This could then be stored in a
// DisplayList line, for benefit of the Info panel.
/// <summary>
/// Internal base-object constructor, called by other constructors.
/// </summary>
private DefSymbol(string label, int value, Source source, Type type)
: base(label, value, source, type) {
Debug.Assert(source == Source.Platform || source == Source.Project);
Debug.Assert(type == Type.ExternalAddr || type == Type.Constant);
Xrefs = new XrefSet();
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="label">Symbol's label.</param>
/// <param name="value">Symbol's value.</param>
/// <param name="source">Symbol source (general point of origin).</param>
/// <param name="type">Symbol type.</param>
/// <param name="formatSubType">Format descriptor sub-type, so we know how the
/// user wants the value to be displayed.</param>
/// <param name="comment">End-of-line comment.</param>
/// <param name="tag">Symbol tag, used for grouping platform symbols.</param>
public DefSymbol(string label, int value, Source source, Type type,
FormatDescriptor.SubType formatSubType, string comment, string tag)
: this(label, value, source, type) {
Debug.Assert(comment != null);
Debug.Assert(tag != null);
// Length doesn't matter; use 1 to get prefab object.
DataDescriptor = FormatDescriptor.Create(1,
FormatDescriptor.Type.NumericLE, formatSubType);
Comment = comment;
Tag = tag;
}
/// <summary>
/// Constructs a DefSymbol from a Symbol and a format descriptor. This is used
/// for project symbols.
/// </summary>
/// <param name="sym">Base symbol.</param>
/// <param name="dfd">Format descriptor.</param>
/// <param name="comment">End-of-line comment.</param>
public DefSymbol(Symbol sym, FormatDescriptor dfd, string comment)
: this(sym.Label, sym.Value, sym.SymbolSource, sym.SymbolType) {
Debug.Assert(comment != null);
DataDescriptor = dfd;
Comment = comment;
Tag = string.Empty;
}
public override string ToString() {
return base.ToString() + ":" + DataDescriptor + ";" + Comment +
(string.IsNullOrEmpty(Tag) ? "" : " [" + Tag + "]");
}
}
}

File diff suppressed because it is too large Load Diff

1224
SourceGenWPF/DisplayList.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,264 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Diagnostics;
using System.IO;
namespace SourceGenWPF {
/// <summary>
/// Manages references to external files, notably symbol files (.sym65) and extension
/// scripts. Identifiers look like "RT:subdir/file.sym65".
///
/// Instances are immutable.
/// </summary>
public class ExternalFile {
private const string INVALID_IDENT = "!!!INVALID!!!"; // probably don't need localization
/// <summary>
/// Pathname separator character for use for file identifiers. We want this
/// to be the same on all platforms, with local conversion, so we should probably be
/// using something like ':' that makes Windows barf. In practice, being rigorous
/// doesn't seem important, and '/' is pretty universal these days. Just don't do \.
/// </summary>
private const char PATH_SEP_CHAR = '/';
private const string RUNTIME_DIR_PREFIX = "RT:";
private const string PROJECT_DIR_PREFIX = "PROJ:";
private enum Location {
Unknown = 0,
RuntimeDir,
ProjectDir
}
// Not sure there's value in tracking the type, except for some validation checks.
private enum Type {
Unknown = 0,
SymbolFile,
ExtensionScript
}
/// <summary>
/// Identifier for this file.
/// </summary>
public string Identifier { get { return mIdent; } }
private string mIdent;
/// <summary>
/// File location.
/// </summary>
private Location mIdentLocation;
/// <summary>
/// File type.
/// </summary>
private Type mIdentType;
/// <summary>
/// Identifier without location prefix or filename extension.
/// </summary>
private string mInnards;
/// <summary>
/// Creates a new ExternalFile instance from the identifier.
/// </summary>
public static ExternalFile CreateFromIdent(string ident) {
if (!DecodeIdent(ident, out Location identLocation, out Type identType,
out string innards)) {
return null;
}
return new ExternalFile(ident, identLocation, identType, innards);
}
/// <summary>
/// Creates a new ExternalFile instance from a full path.
/// </summary>
/// <param name="pathName">Full path of external file, in canonical
/// form.</param>
/// <param name="projectDir">Full path to directory in which project file lives, in
/// canonical form. If the project hasn't been saved yet, pass an empty string.</param>
/// <returns>New object, or null if the path isn't valid.</returns>
public static ExternalFile CreateFromPath(string pathName, string projectDir) {
string stripDir;
string rtDir = RuntimeDataAccess.GetDirectory();
string prefix;
// Check path prefix for RT:, and full directory name for PROJ:.
if (pathName.StartsWith(rtDir)) {
stripDir = rtDir;
prefix = RUNTIME_DIR_PREFIX;
} else if (!string.IsNullOrEmpty(projectDir) &&
Path.GetDirectoryName(pathName) == projectDir) {
stripDir = projectDir;
prefix = PROJECT_DIR_PREFIX;
} else {
Debug.WriteLine("Path not in RuntimeData or project: " + pathName);
return null;
}
// Remove directory component.
string partialPath = pathName.Substring(stripDir.Length);
// If directory string didn't end with '/' or '\\', remove char from start.
if (partialPath[0] == '\\' || partialPath[0] == '/') {
partialPath = partialPath.Substring(1);
}
// Replace canonical path sep with '/'.
partialPath = partialPath.Replace(Path.DirectorySeparatorChar, PATH_SEP_CHAR);
string ident = prefix + partialPath;
Debug.WriteLine("Converted path '" + pathName + "' to ident '" + ident + "'");
return CreateFromIdent(ident);
}
/// <summary>
/// Internal constructor.
/// </summary>
private ExternalFile(string ident, Location identLocation, Type identType,
string innards) {
mIdent = ident;
mIdentLocation = identLocation;
mIdentType = identType;
mInnards = innards;
}
/// <summary>
/// Decodes an ident string into its constituent parts.
/// </summary>
private static bool DecodeIdent(string ident, out Location identLocation,
out Type identType, out string innards) {
identLocation = Location.Unknown;
identType = Type.Unknown;
innards = string.Empty;
int prefixLen;
if (ident.StartsWith(RUNTIME_DIR_PREFIX)) {
identLocation = Location.RuntimeDir;
prefixLen = RUNTIME_DIR_PREFIX.Length;
} else if (ident.StartsWith(PROJECT_DIR_PREFIX)) {
identLocation = Location.ProjectDir;
prefixLen = PROJECT_DIR_PREFIX.Length;
} else {
return false;
}
int extLen;
if (ident.EndsWith(PlatformSymbols.FILENAME_EXT)) {
identType = Type.SymbolFile;
extLen = PlatformSymbols.FILENAME_EXT.Length;
} else if (ident.EndsWith(Sandbox.ScriptManager.FILENAME_EXT)) {
identType = Type.ExtensionScript;
extLen = Sandbox.ScriptManager.FILENAME_EXT.Length;
} else {
return false;
}
// Fail idents with no actual name, e.g. "RT:.cs".
if (ident.Length == prefixLen + extLen) {
return false;
}
innards = ident.Substring(prefixLen, ident.Length - prefixLen - extLen);
return true;
}
/// <summary>
/// Strips the prefix and filename extension off of an identifier.
/// </summary>
/// <returns>Stripped identifier, or null if the identifier was malformed.</returns>
public string GetInnards() {
return mInnards;
}
/// <summary>
/// Converts an identifier to a full path. For PROJ: identifiers, the project
/// directory argument is used.
/// </summary>
/// <param name="ident">Identifier to convert.</param>
/// <param name="projectDir">Full path to directory in which project file lives, in
/// canonical form. If the project hasn't been saved yet, pass an empty string.</param>
/// <returns>Full path, or null if the identifier points to a file outside the
/// directory, or if this is a ProjectDir ident and the project dir isn't set.</returns>
public string GetPathName(string projectDir) {
string dir;
bool subdirAllowed;
switch (mIdentLocation) {
case Location.RuntimeDir:
dir = RuntimeDataAccess.GetDirectory();
subdirAllowed = true;
break;
case Location.ProjectDir:
if (string.IsNullOrEmpty(projectDir)) {
// Shouldn't happen in practice -- we don't create PROJ: identifiers
// unless a project directory has been established.
Debug.Assert(false);
return null;
}
dir = projectDir;
subdirAllowed = false;
break;
default:
Debug.Assert(false);
return null;
}
int extLen = mIdent.IndexOf(':') + 1;
string fullPath = Path.GetFullPath(Path.Combine(dir, mIdent.Substring(extLen)));
// Confirm the file actually lives in the directory. RT: files can be anywhere
// below the RuntimeData directory, while PROJ: files must live in the project
// directory.
if (subdirAllowed) {
dir += Path.DirectorySeparatorChar;
if (!fullPath.StartsWith(dir)) {
Debug.WriteLine("WARNING: ident resolves outside subdir: " + mIdent);
Debug.Assert(false);
return null;
}
} else {
if (dir != Path.GetDirectoryName(fullPath)) {
Debug.WriteLine("WARNING: ident resolves outside dir: " + mIdent);
return null;
}
}
return fullPath;
}
/// <summary>
/// Generates a script DLL name from the ident. If the ident is for a project-scope
/// extension script, the project's file name will be included.
/// </summary>
/// <param name="projectPathName">Full path to project.</param>
/// <returns>DLL filename.</returns>
public string GenerateDllName(string projectFileName) {
switch (mIdentLocation) {
case Location.RuntimeDir:
return "RT_" + mInnards.Replace(PATH_SEP_CHAR, '_') + ".dll";
case Location.ProjectDir:
string noExt = Path.GetFileNameWithoutExtension(projectFileName);
return "PROJ_" + noExt + "_" + mInnards.Replace(PATH_SEP_CHAR, '_') + ".dll";
default:
Debug.Assert(false);
return null;
}
}
}
}

View File

@ -0,0 +1,423 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace SourceGenWPF {
/// <summary>
/// Format descriptor for data items and instruction operands. Instances are immutable.
///
/// A list of these is saved as part of the project definition. Code and data that
/// doesn't have one of these will be formatted with default behavior. For data that
/// means a single hexadecimal byte.
///
/// These are referenced from the project and the Anattribs array. Entries in the
/// latter may come from the project (as specified by the user), or be auto-generated
/// by the data analysis pass.
///
/// 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.
/// </summary>
public class FormatDescriptor {
/// <summary>
/// General data type.
///
/// The UI only allows big-endian values in certain situations. Internally we want
/// to be orthogonal in case the policy changes.
/// </summary>
public enum Type : byte {
Unknown = 0,
REMOVE, // special type, only used by operand editor
Default, // means "unformatted", same effect as not having a FormatDescriptor
NumericLE, // 1-4 byte number, little-endian
NumericBE, // 1-4 byte number, big-endian
String, // character string
Dense, // raw data, represented as compactly as possible
Fill // fill memory with a value
}
/// <summary>
/// Additional data type detail.
///
/// Some things are extracted from the data itself, e.g. we don't need to specify if
/// a string is high- or low-ASCII, or what value to use for Fill.
/// </summary>
public enum SubType : byte {
None = 0,
// NumericLE/BE; default is "raw", which can have a context-specific display format
Hex,
Decimal,
Binary,
Ascii, // aspirational; falls back on hex if data not suited
Address, // wants to be an address, but no symbol defined
Symbol, // symbolic ref; replace with Expression, someday?
// String; default is straight text
Reverse, // plain ASCII in reverse order
CString, // null-terminated
L8String, // 8-bit length prefix
L16String, // 16-bit length prefix
Dci, // Dextral Character Inverted
DciReverse, // DCI with string backwards [deprecated -- no asm supports this]
// Dense; no sub-types
// Fill; default is non-ignore
Ignore // TODO(someday): use this for "don't care" sections
}
private const int MAX_NUMERIC_LEN = 4;
// Create some "stock" descriptors. For simple cases we return one of these
// instead of allocating a new object.
private static FormatDescriptor ONE_DEFAULT = new FormatDescriptor(1,
Type.Default, SubType.None);
private static FormatDescriptor ONE_NONE = new FormatDescriptor(1,
Type.NumericLE, SubType.None);
private static FormatDescriptor ONE_HEX = new FormatDescriptor(1,
Type.NumericLE, SubType.Hex);
private static FormatDescriptor ONE_DECIMAL = new FormatDescriptor(1,
Type.NumericLE, SubType.Decimal);
private static FormatDescriptor ONE_BINARY = new FormatDescriptor(1,
Type.NumericLE, SubType.Binary);
private static FormatDescriptor ONE_ASCII = new FormatDescriptor(1,
Type.NumericLE, SubType.Ascii);
/// <summary>
/// Length, in bytes, of the data to be formatted.
///
/// For an instruction, this must match what the code analyzer found as the length
/// of the entire instruction, or the descriptor will be ignored.
///
/// For data items, this determines the length of the formatted region.
/// </summary>
public int Length { get; private set; }
/// <summary>
/// Primary format. The actual data must match the format:
/// - Numeric values must be 1-4 bytes.
/// - String values must be ASCII characters with a common high bit (although
/// the start or end may diverge from this based on the sub-type).
/// - Fill areas must contain identical bytes.
/// </summary>
public Type FormatType { get; private set; }
/// <summary>
/// Sub-format specifier. Each primary format has specific sub-formats, but we
/// lump them all together for convenience.
/// </summary>
public SubType FormatSubType { get; private set; }
/// <summary>
/// Symbol reference for Type=Numeric SubType=Symbol. null otherwise.
///
/// Numeric values, such as addresses and constants, can be generated with an
/// expression. Currently we only support using a single symbol, but the goal
/// is to allow free-form expressions like "(sym1+sym2+$80)/3".
///
/// If the symbol exists, the symbol's name will be shown, possibly with an adjustment
/// to make the symbol value match the operand or data item.
///
/// Note this reference has a "part" modifier, so we can use it for e.g. "#>label".
/// </summary>
public WeakSymbolRef SymbolRef { get; private set; }
// Crude attempt to see how effective the prefab object creation is. Note we create
// these for DefSymbols, so there will be one prefab for every platform symbol entry.
public static int DebugCreateCount { get; private set; }
public static int DebugPrefabCount { get; private set; }
public static void DebugPrefabBump(int adj=1) {
DebugCreateCount += adj;
DebugPrefabCount += adj;
}
/// <summary>
/// Constructor for base type data item.
/// </summary>
/// <param name="Length">Length, in bytes.</param>
/// <param name="fmt">Format type.</param>
/// <param name="subFmt">Format sub-type.</param>
private FormatDescriptor(int length, Type fmt, SubType subFmt) {
Debug.Assert(length > 0);
Debug.Assert(length <= MAX_NUMERIC_LEN || !IsNumeric);
Debug.Assert(fmt != Type.Default || length == 1);
Length = length;
FormatType = fmt;
FormatSubType = subFmt;
}
/// <summary>
/// Constructor for symbol item.
/// </summary>
/// <param name="length">Length, in bytes.</param>
/// <param name="sym">Weak symbol reference.</param>
/// <param name="isBigEndian">Set to true for big-endian data.</param>
private FormatDescriptor(int length, WeakSymbolRef sym, bool isBigEndian) {
Debug.Assert(sym != null);
Debug.Assert(length > 0 && length <= MAX_NUMERIC_LEN);
Length = length;
FormatType = isBigEndian ? Type.NumericBE : Type.NumericLE;
FormatSubType = SubType.Symbol;
SymbolRef = sym;
}
/// <summary>
/// Returns a descriptor with the requested characteristics. For common cases this
/// returns a pre-allocated object, for less-common cases this allocates a new object.
///
/// Objects are immutable and do not specify a file offset, so they may be re-used
/// by the caller.
/// </summary>
/// <param name="length">Length, in bytes.</param>
/// <param name="fmt">Format type.</param>
/// <param name="subFmt">Format sub-type.</param>
/// <returns>New or pre-allocated descriptor.</returns>
public static FormatDescriptor Create(int length, Type fmt, SubType subFmt) {
DebugCreateCount++;
DebugPrefabCount++;
if (length == 1) {
if (fmt == Type.Default) {
Debug.Assert(subFmt == SubType.None);
return ONE_DEFAULT;
} else if (fmt == Type.NumericLE) {
switch (subFmt) {
case SubType.None:
return ONE_NONE;
case SubType.Hex:
return ONE_HEX;
case SubType.Decimal:
return ONE_DECIMAL;
case SubType.Binary:
return ONE_BINARY;
case SubType.Ascii:
return ONE_ASCII;
}
}
}
// For a new file, this will be mostly strings and Fill.
DebugPrefabCount--;
return new FormatDescriptor(length, fmt, subFmt);
}
/// <summary>
/// Returns a descriptor with a symbol.
/// </summary>
/// <param name="length">Length, in bytes.</param>
/// <param name="sym">Weak symbol reference.</param>
/// <param name="isBigEndian">Set to true for big-endian data.</param>
/// <returns>New or pre-allocated descriptor.</returns>
public static FormatDescriptor Create(int length, WeakSymbolRef sym, bool isBigEndian) {
DebugCreateCount++;
return new FormatDescriptor(length, sym, isBigEndian);
}
/// <summary>
/// True if the descriptor is okay to use on an instruction operand. The CPU only
/// understands little-endian numeric values, so that's all we allow.
/// </summary>
public bool IsValidForInstruction {
get {
switch (FormatType) {
case Type.Default:
case Type.NumericLE:
//case Type.NumericBE:
return true;
default:
return false;
}
}
}
/// <summary>
/// True if the FormatDescriptor has a symbol.
/// </summary>
public bool HasSymbol {
get {
Debug.Assert(SymbolRef == null || (IsNumeric && FormatSubType == SubType.Symbol));
return SymbolRef != null;
}
}
/// <summary>
/// True if the FormatDescriptor is a numeric type (NumericLE or NumericBE).
/// </summary>
public bool IsNumeric {
get {
return FormatType == Type.NumericLE || FormatType == Type.NumericBE;
}
}
/// <summary>
/// True if the FormatDescriptor has a symbol or is Numeric/Address.
/// </summary>
public bool HasSymbolOrAddress {
// Derived from other fields, so you can ignore this in equality tests. This is
// of interest to undo/redo, since changing a symbol reference can affect data scan.
get {
return HasSymbol || FormatSubType == SubType.Address;
}
}
/// <summary>
/// Numeric base specific by format/sub-format. Returns 16 when uncertain.
/// </summary>
public int NumBase {
get {
if (FormatType != Type.NumericLE && FormatType != Type.NumericBE) {
Debug.Assert(false);
return 16;
}
switch (FormatSubType) {
case SubType.None:
case SubType.Hex:
return 16;
case SubType.Decimal:
return 10;
case SubType.Binary:
return 2;
default:
Debug.Assert(false);
return 16;
}
}
}
/// <summary>
/// Returns the FormatSubType enum constant for the specified numeric base.
/// </summary>
/// <param name="numBase">Base (2, 10, or 16).</param>
/// <returns>Enum value.</returns>
public static SubType GetSubTypeForBase(int numBase) {
switch (numBase) {
case 2: return SubType.Binary;
case 10: return SubType.Decimal;
case 16: return SubType.Hex;
default:
Debug.Assert(false);
return SubType.Hex;
}
}
/// <summary>
/// Generates a string describing the format, suitable for use in the UI.
/// </summary>
public string ToUiString() {
// NOTE: this should be made easier to localize
switch (FormatSubType) {
case SubType.None:
switch (FormatType) {
case Type.Default:
case Type.NumericLE:
return "Numeric (little-endian)";
case Type.NumericBE:
return "Numeric (big-endian)";
case Type.String:
return "String (generic)";
case Type.Dense:
return "Dense";
case Type.Fill:
return "Fill";
default:
return "???";
}
case SubType.Hex:
return "Numeric, Hex";
case SubType.Decimal:
return "Numeric, Decimal";
case SubType.Binary:
return "Numeric, Binary";
case SubType.Ascii:
return "ASCII";
case SubType.Address:
return "Address";
case SubType.Symbol:
return "Symbol \"" + SymbolRef.Label + "\"";
case SubType.Reverse:
return "String (reverse)";
case SubType.CString:
return "String (null-term)";
case SubType.L8String:
return "String (1-byte len)";
case SubType.L16String:
return "String (2-byte len)";
case SubType.Dci:
return "String (DCI)";
case SubType.DciReverse:
return "String (RevDCI)";
default:
return "???";
}
}
public override string ToString() {
return "[FmtDesc: len=" + Length + " fmt=" + FormatType + " sub=" + FormatSubType +
" sym=" + SymbolRef + "]";
}
public static bool operator ==(FormatDescriptor a, FormatDescriptor 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.Length == b.Length && a.FormatType == b.FormatType &&
a.FormatSubType == b.FormatSubType && a.SymbolRef == b.SymbolRef;
}
public static bool operator !=(FormatDescriptor a, FormatDescriptor b) {
return !(a == b);
}
public override bool Equals(object obj) {
return obj is FormatDescriptor && this == (FormatDescriptor)obj;
}
public override int GetHashCode() {
int hashCode = 0;
if (SymbolRef != null) {
hashCode = SymbolRef.GetHashCode();
}
hashCode ^= Length;
hashCode ^= (int)FormatType;
hashCode ^= (int)FormatSubType;
return hashCode;
}
/// <summary>
/// Debugging utility function to dump a sorted list of objects.
/// </summary>
public static void DebugDumpSortedList(SortedList<int, FormatDescriptor> list) {
if (list == null) {
Debug.WriteLine("FormatDescriptor list is empty");
return;
}
Debug.WriteLine("FormatDescriptor list (" + list.Count + " entries)");
foreach (KeyValuePair<int, FormatDescriptor> kvp in list) {
int offset = kvp.Key;
FormatDescriptor dfd = kvp.Value;
Debug.WriteLine(" +" + offset.ToString("x6") + ",+" +
(offset + dfd.Length - 1).ToString("x6") + ": " + dfd.FormatType +
"(" + dfd.FormatSubType + ")");
}
}
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.IO;
using CommonUtil;
/*
There are a few different options for viewing help files:
(1) Microsoft HTML Help. Requires writing stuff in a specific way and then running a
tool to turn it into a .chm file, which then requires a help viewer application.
Feels a little weak in terms of future-proofing and cross-platform support.
(2) Plain HTML, using System.Windows.Forms.WebBrowser class. This seems like a nice
way to go, but we need to provide all the standard controls, and it means we have
a web browser running in-process.
(3) Plain HTML, with the Microsoft.Toolkit.Win32.UI.Controls.WinForms.WebView control.
Similar to WebBrowser, but newer and fancier, and probably less portable.
(4) Plain HTML, viewed with the system browser. We outsource the problem. The big
problem here is that the easy/portable way (Process.Start(url)) discards the anchor
part (the bit after '#'). There are workarounds, but they seem to involve dredging
the default browser out of the Registry.
(5) Custom roll-your-own solution. Have you seen this round thing I invented? I'm
calling it a "wheel".
For now I'm going with #4, and dealing with anchors by ignoring them: the help menu item
just opens the TOC, and individual UI items don't have help buttons.
What we need in terms of API is a way to say, "show the help for XYZ". The rest can be
encapsulated here.
*/
namespace SourceGenWPF {
/// <summary>
/// Help viewer API.
/// </summary>
public static class HelpAccess {
private const string HELP_DIR = "Help"; // directory inside RuntimeData
/// <summary>
/// Help topics.
/// </summary>
public enum Topic {
Contents, // table of contents
Main, // main window, general workflow
// Editors
EditLongComment,
}
private static Dictionary<Topic, string> sTopicMap = new Dictionary<Topic, string>() {
{ Topic.Contents, "index.html" },
{ Topic.Main, "main.html" },
{ Topic.EditLongComment, "editor.html#long-comment" }
};
/// <summary>
/// Opens a window with the specified help topic.
/// </summary>
/// <param name="topic"></param>
public static void ShowHelp(Topic topic) {
if (!sTopicMap.TryGetValue(topic, out string fileName)) {
Debug.Assert(false, "Unable to find " + topic + " in map");
return;
}
string helpFilePath = Path.Combine(RuntimeDataAccess.GetDirectory(),
HELP_DIR, fileName);
string url = "file://" + helpFilePath;
//url = url.Replace("#", "%23");
Debug.WriteLine("Requesting help URL: " + url);
ShellCommand.OpenUrl(url);
}
}
}

View File

@ -0,0 +1,276 @@
/*
* 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.Windows.Media;
using System.Text;
namespace SourceGenWPF {
/// <summary>
/// Representation of a multi-line comment, which is a string plus some format directives.
/// Used for long comments and notes.
///
/// Instances are immutable.
/// </summary>
public class MultiLineComment {
/// <summary>
/// If set, sticks a MaxWidth "ruler" at the top, and makes spaces visible.
/// </summary>
public static bool DebugShowRuler { get; set; }
/// <summary>
/// Unformatted text.
/// </summary>
public string Text { get; private set; }
/// <summary>
/// Set to true to render text surrounded by a box of ASCII characters.
/// </summary>
public bool BoxMode { get; private set; }
/// <summary>
/// Maximum line width. Box mode effectively reduces this by four.
/// </summary>
public int MaxWidth { get; private set; }
/// <summary>
/// Background color for notes.
/// </summary>
public Color BackgroundColor { get; private set; }
/// <summary>
/// Constructor. Object will have a max width of 80 and not be boxed.
/// </summary>
/// <param name="text">Unformatted comment text.</param>
public MultiLineComment(string text) {
Debug.Assert(text != null); // empty string is okay
Text = text;
BoxMode = false;
MaxWidth = 80;
BackgroundColor = Color.FromArgb(0, 0, 0, 0);
}
/// <summary>
/// Constructor. Used for long comments.
/// </summary>
/// <param name="text">Unformatted text.</param>
/// <param name="boxMode">Set to true to enable box mode.</param>
/// <param name="maxWidth">Maximum line width.</param>
public MultiLineComment(string text, bool boxMode, int maxWidth) : this(text) {
Debug.Assert((!boxMode && maxWidth > 1) || (boxMode && maxWidth > 5));
BoxMode = boxMode;
MaxWidth = maxWidth;
}
/// <summary>
/// Constructor. Used for notes.
/// </summary>
/// <param name="text">Unformatted text.</param>
/// <param name="bkgndColor">Background color.</param>
public MultiLineComment(string text, Color bkgndColor) : this(text) {
BackgroundColor = bkgndColor;
}
/// <summary>
/// Generates one or more lines of formatted text.
/// </summary>
/// <param name="formatter">Formatter, with comment delimiters.</param>
/// <param name="textPrefix">String to prepend to text before formatting. If this
/// is non-empty, comment delimiters aren't emitted. (Used for notes.)</param>
/// <returns>Array of formatted strings.</returns>
public List<string> FormatText(Asm65.Formatter formatter, string textPrefix) {
const char boxChar = '*';
const char spcRep = '\u2219';
string workString = string.IsNullOrEmpty(textPrefix) ? Text : textPrefix + Text;
List<string> lines = new List<string>();
string linePrefix;
if (!string.IsNullOrEmpty(textPrefix)) {
linePrefix = string.Empty;
} else if (BoxMode) {
linePrefix = formatter.BoxLineCommentDelimiter;
} else {
linePrefix = formatter.FullLineCommentDelimiter;
}
StringBuilder sb = new StringBuilder(MaxWidth);
if (DebugShowRuler) {
for (int i = 0; i < MaxWidth; i++) {
sb.Append((i % 10).ToString());
}
lines.Add(sb.ToString());
sb.Clear();
}
string boxLine, spaces;
if (BoxMode) {
for (int i = 0; i < MaxWidth - linePrefix.Length; i++) {
sb.Append(boxChar);
}
boxLine = sb.ToString();
sb.Clear();
for (int i = 0; i < MaxWidth; i++) {
sb.Append(' ');
}
spaces = sb.ToString();
sb.Clear();
} else {
boxLine = spaces = null;
}
if (BoxMode && workString.Length > 0) {
lines.Add(linePrefix + boxLine);
}
int lineWidth = BoxMode ?
MaxWidth - linePrefix.Length - 4 :
MaxWidth - linePrefix.Length;
int startIndex = 0;
int breakIndex = -1;
for (int i = 0; i < workString.Length; i++) {
// Spaces and hyphens are different. For example, if width is 10,
// "long words<space>more words" becomes:
// 0123456789
// long words
// more words
// However, "long words-more words" becomes:
// long
// words-more
// words
// because the hyphen is retained but the space is discarded.
if (workString[i] == '\r' || workString[i] == '\n') {
// explicit line break, emit line
string str = workString.Substring(startIndex, i - startIndex);
if (DebugShowRuler) { str = str.Replace(' ', spcRep); }
if (BoxMode) {
if (str == "" + boxChar) {
// asterisk on a line by itself means "output row of asterisks"
str = linePrefix + boxLine;
} else {
int padLen = lineWidth - str.Length;
str = linePrefix + boxChar + " " + str +
spaces.Substring(0, padLen + 1) + boxChar;
}
} else {
str = linePrefix + str;
}
lines.Add(str);
// Eat the LF in CRLF. We don't actually work right with just LF,
// because this will consume LFLF, but it's okay to insist that the
// string use CRLF for line breaks.
if (i < workString.Length - 1 && workString[i + 1] == '\n') {
i++;
}
startIndex = i + 1;
breakIndex = -1;
} else if (workString[i] == ' ') {
// can break on a space even if it's one char too far
breakIndex = i;
}
if (i - startIndex >= lineWidth) {
// this character was one too many, break line one back
if (breakIndex <= 0) {
// no break found, just chop it
string str = workString.Substring(startIndex, i - startIndex);
if (DebugShowRuler) { str = str.Replace(' ', spcRep); }
if (BoxMode) {
str = linePrefix + boxChar + " " + str + " " + boxChar;
} else {
str = linePrefix + str;
}
lines.Add(str);
startIndex = i;
} else {
// Copy everything from start to break. If the break was a hyphen,
// we want to keep it.
int adj = 0;
if (workString[breakIndex] == '-') {
adj = 1;
}
string str = workString.Substring(startIndex,
breakIndex + adj - startIndex);
if (DebugShowRuler) { str = str.Replace(' ', spcRep); }
if (BoxMode) {
int padLen = lineWidth - str.Length;
str = linePrefix + boxChar + " " + str +
spaces.Substring(0, padLen + 1) + boxChar;
} else {
str = linePrefix + str;
}
lines.Add(str);
startIndex = breakIndex + 1;
breakIndex = -1;
}
}
if (workString[i] == '-') {
// can break on hyphen if it fits in line
breakIndex = i;
}
}
if (startIndex < workString.Length) {
// Output remainder.
string str = workString.Substring(startIndex, workString.Length - startIndex);
if (DebugShowRuler) { str = str.Replace(' ', spcRep); }
if (BoxMode) {
int padLen = lineWidth - str.Length;
str = linePrefix + boxChar + " " + str +
spaces.Substring(0, padLen + 1) + boxChar;
} else {
str = linePrefix + str;
}
lines.Add(str);
}
if (BoxMode && workString.Length > 0) {
lines.Add(linePrefix + boxLine);
}
return lines;
}
public override string ToString() {
return "MLC box=" + BoxMode + " width=" + MaxWidth + " text='" + Text + "'";
}
public static bool operator ==(MultiLineComment a, MultiLineComment 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.Text.Equals(b.Text) && a.BoxMode == b.BoxMode && a.MaxWidth == b.MaxWidth
&& a.BackgroundColor == b.BackgroundColor;
}
public static bool operator !=(MultiLineComment a, MultiLineComment b) {
return !(a == b);
}
public override bool Equals(object obj) {
return obj is MultiLineComment && this == (MultiLineComment)obj;
}
public override int GetHashCode() {
return Text.GetHashCode() ^ MaxWidth ^ (BoxMode ? 1 : 0) ^ BackgroundColor.GetHashCode();
}
}
}

157
SourceGenWPF/NavStack.cs Normal file
View File

@ -0,0 +1,157 @@
/*
* 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.Text;
namespace SourceGenWPF {
/// <summary>
/// Maintains a record of interesting places we've been.
/// </summary>
public class NavStack {
// If you're at offset 10, and you jump to offset 20, we push offset 10 onto the
// back list. If you hit back, you want to be at offset 10. If you then hit
// forward, you want to jump to offset 20. So how does 20 get on there?
//
// The trick is to record the "from" and "to" position at each step. When moving
// backward we go the previous "from" position. When moving forward we move to
// the next "to" position. This makes the movement asymmetric, but it means that
// that forward movement is always to places we've jumped to, and backward movement
// is to places we jumped away from.
// TODO(someday): this can be simplified(?) to use a pair of stacks, one for moving
// forward, one for moving backward. Traversing the stack requires popping off one
// and pushing onto the other, rather than moving the cursor. No change in
// behavior, but potentially easier to make sense of.
// TODO(someday): record more about what was selected, so e.g. when we move back or
// forward to a Note we can highlight it appropriately.
// TODO(someday): once we have the above, we can change the back button to a pop-up
// list of locations (like the way VS 2017 does it).
private class OffsetPair {
public int From { get; set; }
public int To { get; set; }
public OffsetPair(int from, int to) {
From = from;
To = to;
}
public override string ToString() {
return "[fr=+" + From.ToString("x6") + " to=+" + To.ToString("x6") + "]";
}
}
// Offset stack. Popped items remain in place temporarily.
private List<OffsetPair> mStack = new List<OffsetPair>();
// Current stack position. This is one past the most-recently-pushed element.
private int mCursor = 0;
public NavStack() { }
/// <summary>
/// True if there is an opportunity to pop backward.
/// </summary>
public bool HasBackward {
get {
return mCursor > 0;
}
}
/// <summary>
/// True if there is an opportunity to push forward.
/// </summary>
public bool HasForward {
get {
return mCursor < mStack.Count;
}
}
/// <summary>
/// Clears the back stack.
/// </summary>
public void Clear() {
mStack.Clear();
mCursor = 0;
}
/// <summary>
/// Pops the top entry off the stack. This moves the cursor but doesn't actually
/// remove the item.
/// </summary>
/// <returns>The "from" element of the popped entry.</returns>
public int Pop() {
if (mCursor == 0) {
throw new Exception("Stack is empty");
}
mCursor--;
//Debug.WriteLine("NavStack popped +" + mStack[mCursor] +
// " (now cursor=" + mCursor + ") -- " + this);
return mStack[mCursor].From;
}
/// <summary>
/// Pushes a new entry onto the stack at the cursor. If there were additional
/// entries past the cursor, they will be discarded.
///
/// If the same entry is already at the top of the stack, the entry will not be added.
/// </summary>
/// <param name="fromOffset">File offset associated with line we are moving from.
/// This may be negative if we're moving from a header comment or .EQ directive.</param>
/// <param name="toOffset">File offset associated with line we are moving to. This
/// may be negative if we're moving to the header comment or a .EQ directive.</param>
public void Push(int fromOffset, int toOffset) {
if (mStack.Count > mCursor) {
mStack.RemoveRange(mCursor, mStack.Count - mCursor);
}
OffsetPair newPair = new OffsetPair(fromOffset, toOffset);
mStack.Add(newPair);
mCursor++;
//Debug.WriteLine("NavStack pushed +" + newPair + " -- " + this);
}
/// <summary>
/// Pushes a previous entry back onto the stack.
/// </summary>
/// <returns>The "to" element of the pushed entry.</returns>
public int PushPrevious() {
if (mCursor == mStack.Count) {
throw new Exception("At top of stack");
}
int fwdOff = mStack[mCursor].To;
mCursor++;
//Debug.WriteLine("NavStack pushed prev (now cursor=" + mCursor + ") -- " + this);
return fwdOff;
}
public override string ToString() {
StringBuilder sb = new StringBuilder();
sb.Append("NavStack:");
for (int i = 0; i < mStack.Count; i++) {
if (i == mCursor) {
sb.Append(" [*]");
}
sb.Append(mStack[i]);
}
if (mCursor == mStack.Count) {
sb.Append(" [*]");
}
return sb.ToString();
}
}
}

View File

@ -0,0 +1,254 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using CommonUtil;
using SourceGenWPF.Sandbox;
namespace SourceGenWPF {
/// <summary>
/// Loads and maintains a collection of platform-specific symbols from a ".sym65" file.
/// </summary>
public class PlatformSymbols : IEnumerable<Symbol> {
public const string FILENAME_EXT = ".sym65";
public static readonly string FILENAME_FILTER = Res.Strings.FILE_FILTER_SYM65;
/// <summary>
/// Regex pattern for name/value pairs in symbol file.
///
/// Alphanumeric ASCII + underscore for label, which must start at beginning of line.
/// Value is somewhat arbitrary, but ends if we see a comment delimiter (semicolon).
/// Spaces are allowed between tokens.
///
/// Group 1 is the name, group 2 is '=' or '@', group 3 is the value, group 4 is
/// the comment (optional).
/// </summary>
private const string NAME_VALUE_PATTERN =
@"^([A-Za-z0-9_]+)\s*([@=])\s*([^\ ;]+)\s*(;.*)?$";
private static Regex sNameValueRegex = new Regex(NAME_VALUE_PATTERN);
private const string TAG_CMD = "*TAG";
/// <summary>
/// List of symbols. We keep them sorted by label because labels must be unique.
///
/// Idea: we could retain the end-of-line comments, and add them as comments in the
/// EQU section of the disassembly.
/// </summary>
private SortedList<string, Symbol> mSymbols =
new SortedList<string, Symbol>(Asm65.Label.LABEL_COMPARER);
public PlatformSymbols() { }
// IEnumerable
public IEnumerator<Symbol> GetEnumerator() {
return mSymbols.Values.GetEnumerator();
}
// IEnumerable
IEnumerator IEnumerable.GetEnumerator() {
return mSymbols.Values.GetEnumerator();
}
/// <summary>
/// Loads platform symbols.
/// </summary>
/// <param name="fileIdent">Relative pathname of file to open.</param>
/// <param name="projectDir">Full path to project directory.</param>
/// <param name="report">Report of warnings and errors.</param>
/// <returns>True on success (no errors), false on failure.</returns>
public bool LoadFromFile(string fileIdent, string projectDir, out FileLoadReport report) {
// These files shouldn't be enormous. Do it the easy way.
report = new FileLoadReport(fileIdent);
ExternalFile ef = ExternalFile.CreateFromIdent(fileIdent);
if (ef == null) {
report.Add(FileLoadItem.Type.Error,
CommonUtil.Properties.Resources.ERR_FILE_NOT_FOUND + ": " + fileIdent);
return false;
}
string pathName = ef.GetPathName(projectDir);
if (pathName == null) {
report.Add(FileLoadItem.Type.Error,
Res.Strings.ERR_BAD_IDENT + ": " + fileIdent);
return false;
}
string[] lines;
try {
lines = File.ReadAllLines(pathName);
} catch (IOException ioe) {
Debug.WriteLine("Platform symbol load failed: " + ioe);
report.Add(FileLoadItem.Type.Error,
CommonUtil.Properties.Resources.ERR_FILE_NOT_FOUND + ": " + pathName);
return false;
}
string tag = string.Empty;
int lineNum = 0;
foreach (string line in lines) {
lineNum++; // first line is line 1, says Vim and VisualStudio
if (string.IsNullOrEmpty(line) || line[0] == ';') {
// ignore
} else if (line[0] == '*') {
if (line.StartsWith(TAG_CMD)) {
tag = ParseTag(line);
} else {
// Do something clever with *SYNOPSIS?
Debug.WriteLine("CMD: " + line);
}
} else {
MatchCollection matches = sNameValueRegex.Matches(line);
if (matches.Count == 1) {
//Debug.WriteLine("GOT '" + matches[0].Groups[1] + "' " +
// matches[0].Groups[2] + " '" + matches[0].Groups[3] + "'");
string label = matches[0].Groups[1].Value;
bool isConst = (matches[0].Groups[2].Value[0] == '=');
string badParseMsg;
int value, numBase;
bool parseOk;
if (isConst) {
// Allow various numeric options, and preserve the value.
parseOk = Asm65.Number.TryParseInt(matches[0].Groups[3].Value,
out value, out numBase);
badParseMsg =
CommonUtil.Properties.Resources.ERR_INVALID_NUMERIC_CONSTANT;
} else {
// Allow things like "05/1000". Always hex.
numBase = 16;
parseOk = Asm65.Address.ParseAddress(matches[0].Groups[3].Value,
(1 << 24) - 1, out value);
badParseMsg = CommonUtil.Properties.Resources.ERR_INVALID_ADDRESS;
}
if (!parseOk) {
report.Add(lineNum, FileLoadItem.NO_COLUMN, FileLoadItem.Type.Warning,
badParseMsg);
} else {
string comment = matches[0].Groups[4].Value;
if (comment.Length > 0) {
// remove ';'
comment = comment.Substring(1);
}
FormatDescriptor.SubType subType =
FormatDescriptor.GetSubTypeForBase(numBase);
DefSymbol symDef = new DefSymbol(label, value, Symbol.Source.Platform,
isConst ? Symbol.Type.Constant : Symbol.Type.ExternalAddr,
subType, comment, tag);
if (mSymbols.ContainsKey(label)) {
// This is very easy to do -- just define the same symbol twice
// in the same file. We don't really need to do anything about
// it though.
Debug.WriteLine("NOTE: stomping previous definition of " + label);
}
mSymbols[label] = symDef;
}
} else {
report.Add(lineNum, FileLoadItem.NO_COLUMN, FileLoadItem.Type.Warning,
CommonUtil.Properties.Resources.ERR_SYNTAX);
}
}
}
return !report.HasErrors;
}
/// <summary>
/// Parses the tag out of a tag command line. The tag is pretty much everything after
/// the "*TAG", with whitespace stripped off the start and end. The empty string
/// is valid.
/// </summary>
/// <param name="line">Line to parse.</param>
/// <returns>Tag string.</returns>
private string ParseTag(string line) {
Debug.Assert(line.StartsWith(TAG_CMD));
string tag = line.Substring(TAG_CMD.Length).Trim();
return tag;
}
/// <summary>
/// One-off function to convert the IIgs toolbox function info from NList.Data.TXT
/// to .sym65 format. Doesn't really belong in here, but I'm too lazy to put it
/// anywhere else.
/// </summary>
public static void ConvertNiftyListToolboxFuncs(string inPath, string outPath) {
const string TOOL_START = "* System tools";
const string TOOL_END = "* User tools";
const string PATTERN = @"^([0-9a-fA-F]{4}) (\w+)(.*)";
Regex parseRegex = new Regex(PATTERN);
System.Text.StringBuilder sb = new System.Text.StringBuilder();
string[] lines = File.ReadAllLines(inPath);
List<String> outs = new List<string>();
bool inTools = false;
foreach (string line in lines) {
if (line == TOOL_START) {
inTools = true;
continue;
} else if (line == TOOL_END) {
break;
}
if (!inTools) {
continue;
}
if (line.Substring(5, 4) == "=== ") {
// make this a comment
outs.Add("; " + line.Substring(5));
continue;
}
MatchCollection matches = parseRegex.Matches(line);
if (matches.Count != 1) {
Debug.WriteLine("NConv: bad match on '" + line + "'");
outs.Add("; " + line);
continue;
}
GroupCollection group = matches[0].Groups;
string outStr;
if (matches[0].Groups.Count != 4) {
Debug.WriteLine("NConv: partial match (" + group.Count + ") on '" +
line + "'");
outStr = ";" + group[0];
} else {
sb.Clear();
sb.Append(group[2]);
while (sb.Length < 19) { // not really worried about speed
sb.Append(' ');
}
sb.Append(" = $");
sb.Append(group[1]);
while (sb.Length < 32) {
sb.Append(' ');
}
sb.Append(';');
sb.Append(group[3]);
outs.Add(sb.ToString());
}
}
File.WriteAllLines(outPath, outs);
Debug.WriteLine("NConv complete (" + outs.Count + " lines)");
}
}
}

View File

@ -20,7 +20,7 @@ limitations under the License.
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SourceGenWPF.ProjWin"
mc:Ignorable="d"
Title="6502bench SourceGen" Width="810" Height="510" MinWidth="800" MinHeight="500">
Title="6502bench SourceGen" Width="810" Height="510" MinWidth="800" MinHeight="500" Icon="/SourceGenWPF;component/Res/SourceGenIcon.ico">
<Window.Resources>
<RoutedUICommand x:Key="AssembleCmd" Text="Assemble...">
@ -175,7 +175,7 @@ limitations under the License.
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Image Source="pack://application:,,,/Res/Logo.png" Height="100"/>
<Image Source="/SourceGenWPF;component/Res/Logo.png" Height="100"/>
<!-- <Image Source="Res/Logo.png" Height="100"/> -->
<Grid Margin="8">
<Grid.RowDefinitions>

743
SourceGenWPF/ProjectFile.cs Normal file
View File

@ -0,0 +1,743 @@
/*
* 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.IO;
using System.Text;
using System.Web.Script.Serialization;
using System.Windows.Media;
using CommonUtil;
namespace SourceGenWPF {
/// <summary>
/// Load and save project data from/to a ".dis65" file.
///
/// The various data structures get cloned to avoid situations where you can't freely
/// rename and rearrange code because it's serialized directly to the save file. We
/// want to provide a layer of indirection on fields, output enums as strings rather
/// than digits, etc.
///
/// Also, the JavaScriptSerializer can't deal with integer keys, so we have to convert
/// dictionaries that use those to have string keys.
///
/// On the deserialization side, we want to verify the inputs to avoid anything strange
/// getting loaded that could cause a crash or weird behavior. The goal is to discard
/// anything that looks wrong, providing a useful notification to the user, rather than
/// failing outright.
///
/// I'm expecting the save file format to expand and evolve over time, possibly in
/// incompatible ways that require independent load routines for old and new formats.
/// </summary>
public static class ProjectFile {
public const string FILENAME_EXT = ".dis65";
public static readonly string FILENAME_FILTER = Res.Strings.FILE_FILTER_DIS65;
// This is the version of content we're writing. Bump this any time we add anything.
// This doesn't create forward or backward compatibility issues, because JSON will
// ignore stuff that's in one side but not the other. However, if we're opening a
// newer file in an older program, it's worth letting the user know that some stuff
// may get lost as soon as they save the file.
public const int CONTENT_VERSION = 1;
private static readonly bool ADD_CRLF = true;
/// <summary>
/// Serializes the project and writes it to the specified file.
/// </summary>
/// <param name="proj">Project to serialize.</param>
/// <param name="pathName">Output path name.</param>
/// <param name="errorMessage">Human-readable error string, or an empty string if all
/// went well.</param>
/// <returns>True on success.</returns>
public static bool SerializeToFile(DisasmProject proj, string pathName,
out string errorMessage) {
try {
string serializedData = SerializableProjectFile1.SerializeProject(proj);
if (ADD_CRLF) {
// Add some line breaks. This looks awful, but it makes text diffs
// much more useful.
serializedData = TextUtil.NonQuoteReplace(serializedData, "{", "{\r\n");
serializedData = TextUtil.NonQuoteReplace(serializedData, "},", "},\r\n");
}
// Check to see if the project file is read-only. We want to fail early
// so we don't leave our .TMP file sitting around -- the File.Delete() call
// will fail if the destination is read-only.
if (File.Exists(pathName) &&
(File.GetAttributes(pathName) & FileAttributes.ReadOnly) != 0) {
throw new IOException(string.Format(Res.Strings.ERR_FILE_READ_ONLY_FMT,
pathName));
}
// The BOM is not required or recommended for UTF-8 files, but anecdotal
// evidence suggests that it's sometimes useful. Shouldn't cause any harm
// to have it in the project file. The explicit Encoding.UTF8 argument
// causes it to appear -- WriteAllText normally doesn't.
//
// Write to a temp file, then rename over original after write has succeeded.
string tmpPath = pathName + ".TMP";
File.WriteAllText(tmpPath, serializedData, Encoding.UTF8);
if (File.Exists(pathName)) {
File.Delete(pathName);
}
File.Move(tmpPath, pathName);
errorMessage = string.Empty;
return true;
} catch (Exception ex) {
errorMessage = ex.Message;
return false;
}
}
/// <summary>
/// Reads the specified file and deserializes it into the project.
/// </summary>
/// <param name="pathName">Input path name.</param>
/// <param name="proj">Project to deserialize into.</param>
/// <param name="report">File load report, which may contain errors or warnings.</param>
/// <returns>True on success.</returns>
public static bool DeserializeFromFile(string pathName, DisasmProject proj,
out FileLoadReport report) {
Debug.WriteLine("Deserializing '" + pathName + "'");
report = new FileLoadReport(pathName);
string serializedData;
try {
serializedData = File.ReadAllText(pathName);
} catch (Exception ex) {
report.Add(FileLoadItem.Type.Error, Res.Strings.ERR_PROJECT_LOAD_FAIL +
": " + ex.Message);
return false;
}
if (serializedData.StartsWith(SerializableProjectFile1.MAGIC)) {
// File is a match for SerializableProjectFile1. Strip header and deserialize.
serializedData = serializedData.Substring(SerializableProjectFile1.MAGIC.Length);
try {
bool ok = SerializableProjectFile1.DeserializeProject(serializedData,
proj, report);
if (ok) {
proj.UpdateCpuDef();
}
return ok;
} catch (Exception ex) {
// Ideally this won't happen -- errors should be caught explicitly. This
// is a catch-all to keep us from crashing on expectedly bad input.
report.Add(FileLoadItem.Type.Error,
Res.Strings.ERR_PROJECT_FILE_CORRUPT + ": " + ex);
return false;
}
} else {
report.Add(FileLoadItem.Type.Error, Res.Strings.ERR_NOT_PROJECT_FILE);
return false;
}
}
#if false
public Dictionary<string, object> IntKeysToStrings(Dictionary<int, object> input) {
Dictionary<string, object> output = new Dictionary<string, object>();
foreach (KeyValuePair<int, object> entry in input) {
output.Add(entry.Key.ToString(), entry.Value);
}
return output;
}
public Dictionary<int, object> StringKeysToInts(Dictionary<string, object> input) {
Dictionary<int, object> output = new Dictionary<int, object>();
foreach (KeyValuePair<string, object> entry in input) {
if (!int.TryParse(entry.Key, out int intKey)) {
throw new InvalidOperationException("bad non-int key: " + entry.Key);
}
output.Add(intKey, entry.Value);
}
return output;
}
#endif
}
/// <summary>
/// Somewhat sloppy-looking JSON state dump.
/// </summary>
internal class SerializableProjectFile1 {
// This appears at the top of the file, not as part of the JSON data. The version
// number refers to the file format version, not the application version.
public const string MAGIC = "### 6502bench SourceGen dis65 v1.0 ###";
public SerializableProjectFile1() { }
public class SerProjectProperties {
public string CpuName { get; set; }
public bool IncludeUndocumentedInstr { get; set; }
public int EntryFlags { get; set; }
public string AutoLabelStyle { get; set; }
public SerAnalysisParameters AnalysisParams { get; set; }
public List<string> PlatformSymbolFileIdentifiers { get; set; }
public List<string> ExtensionScriptFileIdentifiers { get; set; }
public SortedList<string, SerDefSymbol> ProjectSyms { get; set; }
public SerProjectProperties() { }
public SerProjectProperties(ProjectProperties props) {
CpuName = Asm65.CpuDef.GetCpuNameFromType(props.CpuType);
IncludeUndocumentedInstr = props.IncludeUndocumentedInstr;
EntryFlags = props.EntryFlags.AsInt;
AutoLabelStyle = props.AutoLabelStyle.ToString();
AnalysisParams = new SerAnalysisParameters(props.AnalysisParams);
// External file identifiers require no conversion.
PlatformSymbolFileIdentifiers = props.PlatformSymbolFileIdentifiers;
ExtensionScriptFileIdentifiers = props.ExtensionScriptFileIdentifiers;
// Convert project-defined symbols to serializable form.
ProjectSyms = new SortedList<string, SerDefSymbol>();
foreach (KeyValuePair<string, DefSymbol> kvp in props.ProjectSyms) {
ProjectSyms.Add(kvp.Key, new SerDefSymbol(kvp.Value));
}
}
}
public class SerAnalysisParameters {
public bool AnalyzeUncategorizedData { get; set; }
public int MinCharsForString { get; set; }
public bool SeekNearbyTargets { get; set; }
public SerAnalysisParameters() { }
public SerAnalysisParameters(ProjectProperties.AnalysisParameters src) {
AnalyzeUncategorizedData = src.AnalyzeUncategorizedData;
MinCharsForString = src.MinCharsForString;
SeekNearbyTargets = src.SeekNearbyTargets;
}
}
public class SerAddressMap {
public int Offset { get; set; }
public int Addr { get; set; }
// Length is computed field, no need to serialize
public SerAddressMap() { }
public SerAddressMap(AddressMap.AddressMapEntry ent) {
Offset = ent.Offset;
Addr = ent.Addr;
}
}
public class SerTypeHintRange {
public int Low { get; set; }
public int High { get; set; }
public string Hint { get; set; }
public SerTypeHintRange() { }
public SerTypeHintRange(int low, int high, string hintStr) {
Low = low;
High = high;
Hint = hintStr;
}
}
public class SerMultiLineComment {
// NOTE: Text must be CRLF at line breaks.
public string Text { get; set; }
public bool BoxMode { get; set; }
public int MaxWidth { get; set; }
public int BackgroundColor { get; set; }
public SerMultiLineComment() { }
public SerMultiLineComment(MultiLineComment mlc) {
Text = mlc.Text;
BoxMode = mlc.BoxMode;
MaxWidth = mlc.MaxWidth;
BackgroundColor = ColorToInt(mlc.BackgroundColor);
}
}
public class SerSymbol {
public string Label { get; set; }
public int Value { get; set; }
public string Source { get; set; }
public string Type { get; set; }
public SerSymbol() { }
public SerSymbol(Symbol sym) {
Label = sym.Label;
Value = sym.Value;
Source = sym.SymbolSource.ToString();
Type = sym.SymbolType.ToString();
}
}
public class SerFormatDescriptor {
public int Length { get; set; }
public string Format { get; set; }
public string SubFormat { get; set; }
public SerWeakSymbolRef SymbolRef { get; set; }
public SerFormatDescriptor() { }
public SerFormatDescriptor(FormatDescriptor dfd) {
Length = dfd.Length;
Format = dfd.FormatType.ToString();
SubFormat = dfd.FormatSubType.ToString();
if (dfd.SymbolRef != null) {
SymbolRef = new SerWeakSymbolRef(dfd.SymbolRef);
}
}
}
public class SerWeakSymbolRef {
public string Label { get; set; }
public string Part { get; set; }
public SerWeakSymbolRef() { }
public SerWeakSymbolRef(WeakSymbolRef weakSym) {
Label = weakSym.Label;
Part = weakSym.ValuePart.ToString();
}
}
public class SerDefSymbol : SerSymbol {
public SerFormatDescriptor DataDescriptor { get; set; }
public string Comment { get; set; }
public SerDefSymbol() { }
public SerDefSymbol(DefSymbol defSym) : base(defSym) {
DataDescriptor = new SerFormatDescriptor(defSym.DataDescriptor);
Comment = defSym.Comment;
}
}
// Fields are serialized to/from JSON. Do not change the field names.
public int _ContentVersion { get; set; }
public int FileDataLength { get; set; }
public int FileDataCrc32 { get; set; }
public SerProjectProperties ProjectProps { get; set; }
public List<SerAddressMap> AddressMap { get; set; }
public List<SerTypeHintRange> TypeHints { get; set; }
public Dictionary<string, int> StatusFlagOverrides { get; set; }
public Dictionary<string, string> Comments { get; set; }
public Dictionary<string, SerMultiLineComment> LongComments { get; set; }
public Dictionary<string, SerMultiLineComment> Notes { get; set; }
public Dictionary<string, SerSymbol> UserLabels { get; set; }
public Dictionary<string, SerFormatDescriptor> OperandFormats { get; set; }
/// <summary>
/// Serializes a DisasmProject into an augmented JSON string.
/// </summary>
/// <param name="proj">Project to serialize.</param>
/// <returns>Augmented JSON string.</returns>
public static string SerializeProject(DisasmProject proj) {
StringBuilder sb = new StringBuilder();
sb.Append(MAGIC); // augment with version string, which will be stripped
sb.Append("\r\n"); // will be ignored by deserializer; might get converted to \n
SerializableProjectFile1 spf = new SerializableProjectFile1();
spf._ContentVersion = ProjectFile.CONTENT_VERSION;
Debug.Assert(proj.FileDataLength == proj.FileData.Length);
spf.FileDataLength = proj.FileDataLength;
spf.FileDataCrc32 = (int)proj.FileDataCrc32;
// Convert AddressMap to serializable form.
spf.AddressMap = new List<SerAddressMap>();
foreach (AddressMap.AddressMapEntry ent in proj.AddrMap) {
spf.AddressMap.Add(new SerAddressMap(ent));
}
// Reduce TypeHints to a collection of ranges. Output the type enum as a string
// so we're not tied to a specific value.
spf.TypeHints = new List<SerTypeHintRange>();
TypedRangeSet trs = new TypedRangeSet();
for (int i = 0; i < proj.TypeHints.Length; i++) {
trs.Add(i, (int)proj.TypeHints[i]);
}
IEnumerator<TypedRangeSet.TypedRange> iter = trs.RangeListIterator;
while (iter.MoveNext()) {
if (iter.Current.Type == (int)CodeAnalysis.TypeHint.NoHint) {
continue;
}
spf.TypeHints.Add(new SerTypeHintRange(iter.Current.Low, iter.Current.High,
((CodeAnalysis.TypeHint)iter.Current.Type).ToString()));
}
// Convert StatusFlagOverrides to serializable form. Just write the state out
// as an integer... not expecting it to change. If it does, we can convert.
spf.StatusFlagOverrides = new Dictionary<string, int>();
for (int i = 0; i < proj.StatusFlagOverrides.Length; i++) {
if (proj.StatusFlagOverrides[i] == Asm65.StatusFlags.DefaultValue) {
continue;
}
spf.StatusFlagOverrides.Add(i.ToString(), proj.StatusFlagOverrides[i].AsInt);
}
// Convert Comments to serializable form.
spf.Comments = new Dictionary<string, string>();
for (int i = 0; i < proj.Comments.Length; i++) {
if (string.IsNullOrEmpty(proj.Comments[i])) {
continue;
}
spf.Comments.Add(i.ToString(), proj.Comments[i]);
}
// Convert multi-line comments to serializable form.
spf.LongComments = new Dictionary<string, SerMultiLineComment>();
foreach (KeyValuePair<int, MultiLineComment> kvp in proj.LongComments) {
spf.LongComments.Add(kvp.Key.ToString(), new SerMultiLineComment(kvp.Value));
}
// Convert multi-line notes to serializable form.
spf.Notes = new Dictionary<string, SerMultiLineComment>();
foreach (KeyValuePair<int, MultiLineComment> kvp in proj.Notes) {
spf.Notes.Add(kvp.Key.ToString(), new SerMultiLineComment(kvp.Value));
}
// Convert user-defined labels to serializable form.
spf.UserLabels = new Dictionary<string, SerSymbol>();
foreach (KeyValuePair<int,Symbol> kvp in proj.UserLabels) {
spf.UserLabels.Add(kvp.Key.ToString(), new SerSymbol(kvp.Value));
}
// Convert operand and data item format descriptors to serializable form.
spf.OperandFormats = new Dictionary<string, SerFormatDescriptor>();
foreach (KeyValuePair<int,FormatDescriptor> kvp in proj.OperandFormats) {
spf.OperandFormats.Add(kvp.Key.ToString(), new SerFormatDescriptor(kvp.Value));
}
spf.ProjectProps = new SerProjectProperties(proj.ProjectProps);
JavaScriptSerializer ser = new JavaScriptSerializer();
string cereal = ser.Serialize(spf);
sb.Append(cereal);
// Stick a linefeed at the end. Makes git happier.
sb.Append("\r\n");
return sb.ToString();
}
/// <summary>
/// Deserializes an augmented JSON string into a DisasmProject.
/// </summary>
/// <param name="cereal">Serialized data.</param>
/// <param name="proj">Project to populate.</param>
/// <param name="report">Error report object.</param>
/// <returns>True on success, false on fatal error.</returns>
public static bool DeserializeProject(string cereal, DisasmProject proj,
FileLoadReport report) {
JavaScriptSerializer ser = new JavaScriptSerializer();
SerializableProjectFile1 spf;
try {
spf = ser.Deserialize<SerializableProjectFile1>(cereal);
} catch (Exception ex) {
// The deserializer seems to be stuffing the entire data stream into the
// exception, which we don't really want, so cap it at 256 chars.
string msg = ex.Message;
if (msg.Length > 256) {
msg = msg.Substring(0, 256) + " [...]";
}
report.Add(FileLoadItem.Type.Error, Res.Strings.ERR_PROJECT_FILE_CORRUPT +
": " + msg);
return false;
}
if (spf._ContentVersion > ProjectFile.CONTENT_VERSION) {
// Post a warning.
report.Add(FileLoadItem.Type.Notice, Res.Strings.PROJECT_FROM_NEWER_APP);
}
if (spf.FileDataLength <= 0) {
report.Add(FileLoadItem.Type.Error, Res.Strings.ERR_BAD_FILE_LENGTH +
": " + spf.FileDataLength);
return false;
}
// Initialize the object and set a few simple items.
proj.Initialize(spf.FileDataLength);
proj.SetFileCrc((uint)spf.FileDataCrc32);
// Deserialize ProjectProperties: misc items.
proj.ProjectProps.CpuType = Asm65.CpuDef.GetCpuTypeFromName(spf.ProjectProps.CpuName);
proj.ProjectProps.IncludeUndocumentedInstr = spf.ProjectProps.IncludeUndocumentedInstr;
proj.ProjectProps.EntryFlags = Asm65.StatusFlags.FromInt(spf.ProjectProps.EntryFlags);
if (Enum.TryParse<AutoLabel.Style>(spf.ProjectProps.AutoLabelStyle,
out AutoLabel.Style als)) {
proj.ProjectProps.AutoLabelStyle = als;
} else {
// unknown value, leave as default
}
proj.ProjectProps.AnalysisParams = new ProjectProperties.AnalysisParameters();
proj.ProjectProps.AnalysisParams.AnalyzeUncategorizedData =
spf.ProjectProps.AnalysisParams.AnalyzeUncategorizedData;
proj.ProjectProps.AnalysisParams.MinCharsForString =
spf.ProjectProps.AnalysisParams.MinCharsForString;
proj.ProjectProps.AnalysisParams.SeekNearbyTargets =
spf.ProjectProps.AnalysisParams.SeekNearbyTargets;
// Deserialize ProjectProperties: external file identifiers.
Debug.Assert(proj.ProjectProps.PlatformSymbolFileIdentifiers.Count == 0);
foreach (string str in spf.ProjectProps.PlatformSymbolFileIdentifiers) {
proj.ProjectProps.PlatformSymbolFileIdentifiers.Add(str);
}
Debug.Assert(proj.ProjectProps.ExtensionScriptFileIdentifiers.Count == 0);
foreach (string str in spf.ProjectProps.ExtensionScriptFileIdentifiers) {
proj.ProjectProps.ExtensionScriptFileIdentifiers.Add(str);
}
// Deserialize ProjectProperties: project symbols.
foreach (KeyValuePair<string, SerDefSymbol> kvp in spf.ProjectProps.ProjectSyms) {
if (!CreateSymbol(kvp.Value, report, out Symbol sym)) {
continue;
}
if (!CreateFormatDescriptor(kvp.Value.DataDescriptor, report,
out FormatDescriptor dfd)) {
continue;
}
proj.ProjectProps.ProjectSyms[sym.Label] =
new DefSymbol(sym, dfd, kvp.Value.Comment);
}
// Deserialize address map.
foreach (SerAddressMap addr in spf.AddressMap) {
proj.AddrMap.Set(addr.Offset, addr.Addr);
}
// Deserialize type hints. Default value in new array as NoHint, so we don't
// need to write those. They should not have been recorded in the file.
foreach (SerTypeHintRange range in spf.TypeHints) {
if (range.Low < 0 || range.High >= spf.FileDataLength || range.Low > range.High) {
report.Add(FileLoadItem.Type.Warning, Res.Strings.ERR_BAD_RANGE +
": " + Res.Strings.PROJECT_FIELD_TYPE_HINT +
" +" + range.Low.ToString("x6") + " - +" + range.High.ToString("x6"));
continue;
}
CodeAnalysis.TypeHint hint;
try {
hint = (CodeAnalysis.TypeHint) Enum.Parse(
typeof(CodeAnalysis.TypeHint), range.Hint);
} catch (ArgumentException) {
report.Add(FileLoadItem.Type.Warning, Res.Strings.ERR_BAD_TYPE_HINT +
": " + range.Hint);
continue;
}
for (int i = range.Low; i <= range.High; i++) {
proj.TypeHints[i] = hint;
}
}
// Deserialize status flag overrides.
foreach (KeyValuePair<string,int> kvp in spf.StatusFlagOverrides) {
if (!ParseValidateKey(kvp.Key, spf.FileDataLength,
Res.Strings.PROJECT_FIELD_STATUS_FLAGS, report, out int intKey)) {
continue;
}
proj.StatusFlagOverrides[intKey] = Asm65.StatusFlags.FromInt(kvp.Value);
}
// Deserialize comments.
foreach (KeyValuePair<string,string> kvp in spf.Comments) {
if (!ParseValidateKey(kvp.Key, spf.FileDataLength,
Res.Strings.PROJECT_FIELD_COMMENT, report, out int intKey)) {
continue;
}
proj.Comments[intKey] = kvp.Value;
}
// Deserialize long comments.
foreach (KeyValuePair<string, SerMultiLineComment> kvp in spf.LongComments) {
if (!ParseValidateKey(kvp.Key, spf.FileDataLength,
Res.Strings.PROJECT_FIELD_LONG_COMMENT, report, out int intKey)) {
continue;
}
proj.LongComments[intKey] = new MultiLineComment(kvp.Value.Text,
kvp.Value.BoxMode, kvp.Value.MaxWidth);
}
// Deserialize notes.
foreach (KeyValuePair<string, SerMultiLineComment> kvp in spf.Notes) {
if (!ParseValidateKey(kvp.Key, spf.FileDataLength,
Res.Strings.PROJECT_FIELD_NOTE, report, out int intKey)) {
continue;
}
proj.Notes[intKey] = new MultiLineComment(kvp.Value.Text,
ColorFromInt(kvp.Value.BackgroundColor));
}
// Deserialize user-defined labels.
SortedList<string, string> labelDupCheck =
new SortedList<string, string>(spf.UserLabels.Count);
foreach (KeyValuePair<string, SerSymbol> kvp in spf.UserLabels) {
if (!ParseValidateKey(kvp.Key, spf.FileDataLength,
Res.Strings.PROJECT_FIELD_USER_LABEL, report, out int intKey)) {
continue;
}
Symbol.Source source;
Symbol.Type type;
try {
source = (Symbol.Source)Enum.Parse(typeof(Symbol.Source), kvp.Value.Source);
type = (Symbol.Type)Enum.Parse(typeof(Symbol.Type), kvp.Value.Type);
if (source != Symbol.Source.User) {
// User labels are always source=user. I don't think it really matters,
// but best to keep junk out.
throw new Exception("wrong source for user label");
}
} catch (ArgumentException) {
report.Add(FileLoadItem.Type.Warning, Res.Strings.ERR_BAD_SYMBOL_ST +
": " + kvp.Value.Source + "/" + kvp.Value.Type);
continue;
}
// Check for duplicate labels. We only want to compare label strings, so we
// can't test UserLabels.ContainsValue (which might be a linear search anyway).
// Dump the labels into a sorted list.
if (labelDupCheck.ContainsKey(kvp.Value.Label)) {
report.Add(FileLoadItem.Type.Warning,
string.Format(Res.Strings.ERR_DUPLICATE_LABEL_FMT,
kvp.Value.Label, intKey));
continue;
}
labelDupCheck.Add(kvp.Value.Label, string.Empty);
proj.UserLabels[intKey] = new Symbol(kvp.Value.Label, kvp.Value.Value,
source, type);
}
// Deserialize operand format descriptors.
foreach (KeyValuePair<string,SerFormatDescriptor> kvp in spf.OperandFormats) {
if (!ParseValidateKey(kvp.Key, spf.FileDataLength,
Res.Strings.PROJECT_FIELD_OPERAND_FORMAT, report,
out int intKey)) {
continue;
}
if (!CreateFormatDescriptor(kvp.Value, report, out FormatDescriptor dfd)) {
continue;
}
if (intKey < 0 || intKey + dfd.Length > spf.FileDataLength) {
report.Add(FileLoadItem.Type.Warning,
string.Format(Res.Strings.ERR_BAD_FD_FMT, intKey));
continue;
}
// TODO(maybe): check to see if the descriptor straddles an address change.
// Not fatal but it'll make things look weird.
proj.OperandFormats[intKey] = dfd;
}
return true;
}
/// <summary>
/// Creates a Symbol from a SerSymbol.
/// </summary>
/// <param name="ssym">Deserialized data.</param>
/// <param name="report">Error report object.</param>
/// <param name="outSym"></param>
/// <returns>True on success.</returns>
private static bool CreateSymbol(SerSymbol ssym, FileLoadReport report,
out Symbol outSym) {
outSym = null;
Symbol.Source source;
Symbol.Type type;
try {
source = (Symbol.Source)Enum.Parse(typeof(Symbol.Source), ssym.Source);
type = (Symbol.Type)Enum.Parse(typeof(Symbol.Type), ssym.Type);
} catch (ArgumentException) {
report.Add(FileLoadItem.Type.Warning, Res.Strings.ERR_BAD_SYMBOL_ST +
": " + ssym.Source + "/" + ssym.Type);
return false;
}
outSym = new Symbol(ssym.Label, ssym.Value, source, type/*, ssym.IsExport*/);
return true;
}
/// <summary>
/// Creates a FormatDescriptor from a SerFormatDescriptor.
/// </summary>
/// <param name="sfd">Deserialized data.</param>
/// <param name="report">Error report object.</param>
/// <param name="dfd">Created FormatDescriptor.</param>
/// <returns>True on success.</returns>
private static bool CreateFormatDescriptor(SerFormatDescriptor sfd,
FileLoadReport report, out FormatDescriptor dfd) {
dfd = null;
FormatDescriptor.Type format;
FormatDescriptor.SubType subFormat;
try {
format = (FormatDescriptor.Type)Enum.Parse(
typeof(FormatDescriptor.Type), sfd.Format);
subFormat = (FormatDescriptor.SubType)Enum.Parse(
typeof(FormatDescriptor.SubType), sfd.SubFormat);
} catch (ArgumentException) {
report.Add(FileLoadItem.Type.Warning, Res.Strings.ERR_BAD_FD_TYPE +
": " + sfd.Format + "/" + sfd.SubFormat);
return false;
}
if (sfd.SymbolRef == null) {
dfd = FormatDescriptor.Create(sfd.Length, format, subFormat);
} else {
WeakSymbolRef.Part part;
try {
part = (WeakSymbolRef.Part)Enum.Parse(
typeof(WeakSymbolRef.Part), sfd.SymbolRef.Part);
} catch (ArgumentException) {
report.Add(FileLoadItem.Type.Warning,
Res.Strings.ERR_BAD_SYMREF_PART +
": " + sfd.SymbolRef.Part);
return false;
}
dfd = FormatDescriptor.Create(sfd.Length,
new WeakSymbolRef(sfd.SymbolRef.Label, part),
format == FormatDescriptor.Type.NumericBE);
}
return true;
}
/// <summary>
/// Parses an integer key that was stored as a string, and checks to see if the
/// value falls within an acceptable range.
/// </summary>
/// <param name="keyStr">Integer key, in string form.</param>
/// <param name="fileLen">Length of file, for range check.</param>
/// <param name="fieldName">Name of field, for error messages.</param>
/// <param name="report">Error report object.</param>
/// <param name="intKey">Returned integer key.</param>
/// <returns>True on success, false on failure.</returns>
private static bool ParseValidateKey(string keyStr, int fileLen, string fieldName,
FileLoadReport report, out int intKey) {
if (!int.TryParse(keyStr, out intKey)) {
report.Add(FileLoadItem.Type.Warning,
Res.Strings.ERR_INVALID_INT_VALUE + " (" +
fieldName + ": " + keyStr + ")");
return false;
}
// Shouldn't allow DisplayList.Line.HEADER_COMMENT_OFFSET on anything but
// LongComment. Maybe "bool allowNegativeKeys"?
if (intKey < fileLen &&
(intKey >= 0 || intKey == DisplayList.Line.HEADER_COMMENT_OFFSET)) {
return true;
} else {
report.Add(FileLoadItem.Type.Warning,
Res.Strings.ERR_INVALID_KEY_VALUE +
" (" + fieldName + ": " + intKey + ")");
return false;
}
}
private static int ColorToInt(Color color) {
return (color.A << 24) | (color.R << 16) | (color.G << 8) | color.B;
}
private static Color ColorFromInt(int colorInt) {
return Color.FromArgb((byte)(colorInt >> 24), (byte)(colorInt >> 16),
(byte)(colorInt >> 8), (byte)colorInt);
}
}
}

View File

@ -0,0 +1,134 @@
/*
* 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;
namespace SourceGenWPF {
/// <summary>
/// A collection of project properties.
///
/// The class is mutable, but may only be modified by DisasmProject.ApplyChanges or
/// the deserializer.
///
/// All fields are explicitly handled by the ProjectFile serializer.
/// </summary>
public class ProjectProperties {
//
// NOTE:
// If you add or modify a member, make sure to update the copy constructor and
// add serialization code to ProjectFile.
//
/// <summary>
/// Some parameters we feed to the analyzers.
/// </summary>
public class AnalysisParameters {
public bool AnalyzeUncategorizedData { get; set; }
public int MinCharsForString { get; set; }
public bool SeekNearbyTargets { get; set; }
public AnalysisParameters() {
AnalyzeUncategorizedData = true;
MinCharsForString = DataAnalysis.DEFAULT_MIN_STRING_LENGTH;
SeekNearbyTargets = true;
}
public AnalysisParameters(AnalysisParameters src) {
AnalyzeUncategorizedData = src.AnalyzeUncategorizedData;
MinCharsForString = src.MinCharsForString;
SeekNearbyTargets = src.SeekNearbyTargets;
}
}
/// <summary>
/// Configured CPU type.
/// </summary>
public Asm65.CpuDef.CpuType CpuType { get; set; }
/// <summary>
/// Should we include undocumented instructions?
/// </summary>
public bool IncludeUndocumentedInstr { get; set; }
/// <summary>
/// Initial status flags at entry points.
/// </summary>
public Asm65.StatusFlags EntryFlags { get; set; }
/// <summary>
/// Naming style for auto-generated labels.
/// </summary>
public AutoLabel.Style AutoLabelStyle { get; set; }
/// <summary>
/// Configurable parameters for the analyzers.
/// </summary>
public AnalysisParameters AnalysisParams { get; set; }
/// <summary>
/// The identifiers of the platform symbol files we want to load symbols from.
/// </summary>
public List<string> PlatformSymbolFileIdentifiers { get; private set; }
/// <summary>
/// The identifiers of the extension scripts we want to load.
/// </summary>
public List<string> ExtensionScriptFileIdentifiers { get; private set; }
/// <summary>
/// Symbols defined at the project level. These get merged with PlatformSyms.
/// The list key is the symbol's label.
/// </summary>
public SortedList<string, DefSymbol> ProjectSyms { get; private set; }
/// <summary>
/// Nullary constructor.
/// </summary>
public ProjectProperties() {
AnalysisParams = new AnalysisParameters();
PlatformSymbolFileIdentifiers = new List<string>();
ExtensionScriptFileIdentifiers = new List<string>();
ProjectSyms = new SortedList<string, DefSymbol>();
}
/// <summary>
/// Copy constructor.
/// </summary>
/// <param name="src">Object to clone.</param>
public ProjectProperties(ProjectProperties src) : this() {
CpuType = src.CpuType;
IncludeUndocumentedInstr = src.IncludeUndocumentedInstr;
EntryFlags = src.EntryFlags;
AutoLabelStyle = src.AutoLabelStyle;
AnalysisParams = new AnalysisParameters(src.AnalysisParams);
// Clone PlatformSymbolFileIdentifiers
foreach (string fileName in src.PlatformSymbolFileIdentifiers) {
PlatformSymbolFileIdentifiers.Add(fileName);
}
// Clone ExtensionScriptFileIdentifiers
foreach (string fileName in src.ExtensionScriptFileIdentifiers) {
ExtensionScriptFileIdentifiers.Add(fileName);
}
// Clone ProjectSyms
foreach (KeyValuePair<string, DefSymbol> kvp in src.ProjectSyms) {
ProjectSyms[kvp.Key] = kvp.Value;
}
}
}
}

897
SourceGenWPF/PseudoOp.cs Normal file
View File

@ -0,0 +1,897 @@
/*
* 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 SourceGenWPF {
/// <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 operand.
/// </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;
}
}
/// <summary>
/// Pseudo-op name collection. Name strings may be null.
/// </summary>
public class PseudoOpNames {
public string EquDirective { get; set; }
public string OrgDirective { get; set; }
public string RegWidthDirective { get; 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 StrGenericHi { get; set; }
public string StrReverse { get; set; }
public string StrReverseHi { get; set; }
public string StrLen8 { get; set; }
public string StrLen8Hi { get; set; }
public string StrLen16 { get; set; }
public string StrLen16Hi { get; set; }
public string StrNullTerm { get; set; }
public string StrNullTermHi { get; set; }
public string StrDci { get; set; }
public string StrDciHi { get; set; }
public string StrDciReverse { get; set; }
public string StrDciReverseHi { get; set; }
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 ".!!?";
}
}
public PseudoOpNames GetCopy() {
// Do it the lazy way.
return Deserialize(Serialize());
}
/// <summary>
/// Merges the non-null, non-empty strings in "other" into this instance.
/// </summary>
public void Merge(PseudoOpNames other) {
// Lots of fields, we don't do this often... use reflection.
Type type = GetType();
PropertyInfo[] props = type.GetProperties();
foreach (PropertyInfo pi in props) {
string str = (string)pi.GetValue(other);
if (string.IsNullOrEmpty(str)) {
continue;
}
pi.SetValue(this, str);
}
}
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();
return ser.Serialize(this);
}
public static PseudoOpNames Deserialize(string cereal) {
JavaScriptSerializer ser = new JavaScriptSerializer();
try {
return ser.Deserialize<PseudoOpNames>(cereal);
} catch (Exception ex) {
Debug.WriteLine("PseudoOpNames deserialization failed: " + ex.Message);
return new PseudoOpNames();
}
}
}
/// <summary>
/// Some reasonable defaults for on-screen display. The object is mutable, so make
/// a copy of it.
/// </summary>
public static readonly PseudoOpNames sDefaultPseudoOpNames = new PseudoOpNames() {
EquDirective = ".eq",
OrgDirective = ".org",
RegWidthDirective = ".rwid",
DefineData1 = ".dd1",
DefineData2 = ".dd2",
DefineData3 = ".dd3",
DefineData4 = ".dd4",
DefineBigData2 = ".dbd2",
DefineBigData3 = ".dbd3",
DefineBigData4 = ".dbd4",
Fill = ".fill",
Dense = ".bulk",
StrGeneric = ".str",
StrGenericHi = ".strh",
StrReverse = ".rstr",
StrReverseHi = ".rstrh",
StrLen8 = ".l1str",
StrLen8Hi = ".l1strh",
StrLen16 = ".l2str",
StrLen16Hi = ".l2strh",
StrNullTerm = ".zstr",
StrNullTermHi = ".zstrh",
StrDci = ".dstr",
StrDciHi = ".dstrh",
StrDciReverse = ".rdstr",
StrDciReverseHi = ".rdstrh",
};
/// <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, FormatDescriptor dfd) {
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;
}
case FormatDescriptor.Type.String: {
// Subtract two chars, to leave room for start/end delimiter. We use
// non-ASCII delimiters on-screen, so there's nothing to escape there.
int maxLen = MAX_OPERAND_LEN - 2;
// Remove leading length or trailing null byte from string length.
int textLen = dfd.Length;
switch (dfd.FormatSubType) {
case FormatDescriptor.SubType.None:
case FormatDescriptor.SubType.Dci:
case FormatDescriptor.SubType.Reverse:
case FormatDescriptor.SubType.DciReverse:
break;
case FormatDescriptor.SubType.CString:
case FormatDescriptor.SubType.L8String:
textLen--;
break;
case FormatDescriptor.SubType.L16String:
textLen -= 2;
break;
default:
Debug.Assert(false);
break;
}
int strLen = (textLen + maxLen - 1) / maxLen;
if (strLen == 0) {
// Empty string, but we still need to output a line.
strLen = 1;
}
return strLen;
}
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 long comments, 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();
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;
case FormatDescriptor.Type.String:
// It's hard to do strings in single-line pieces because of prefix lengths,
// terminating nulls, DCI polarity, and reverse-order strings. We
// really just want to convert the whole thing to a run of chars
// and then pull out a chunk. As an optimization we can handle
// generic strings (subtype=None) more efficiently, which should solve
// the problem of massive strings created by auto-analysis.
if (dfd.FormatSubType == FormatDescriptor.SubType.None) {
int maxPerLine = MAX_OPERAND_LEN - 2;
offset += subIndex * maxPerLine;
length -= subIndex * maxPerLine;
if (length > maxPerLine) {
length = maxPerLine;
}
char[] ltext = BytesToChars(formatter, opNames, dfd.FormatSubType, data,
offset, length, out string lpopcode, out int unused);
po.Opcode = lpopcode;
po.Operand = "\u201c" + new string(ltext) + "\u201d";
} else {
char[] text = BytesToChars(formatter, opNames, dfd.FormatSubType, data,
offset, length, out string popcode, out int showHexZeroes);
if (showHexZeroes == 1) {
po.Opcode = opNames.DefineData1;
po.Operand = formatter.FormatHexValue(0, 2);
} else if (showHexZeroes == 2) {
po.Opcode = opNames.DefineData2;
po.Operand = formatter.FormatHexValue(0, 4);
} else {
Debug.Assert(showHexZeroes == 0);
po.Opcode = popcode;
List<PseudoOut> outList = new List<PseudoOut>();
GenerateTextLines(text, "\u201c", "\u201d", 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 characters,
/// stripping the high bit. Framing data, such as leading lengths and trailing nulls,
/// are not shown.
/// </summary>
/// <param name="formatter">Formatter object.</param>
/// <param name="subType">String sub-type.</param>
/// <param name="data">File data.</param>
/// <param name="offset">Offset, within data, of start of string.</param>
/// <param name="length">Number of bytes to convert.</param>
/// <param name="popcode">Pseudo-opcode string.</param>
/// <param name="showHexZeroes">If nonzero, show 1+ zeroes (representing a leading
/// length or null-termination) instead of an empty string.</param>
/// <returns>Array of characters with string data.</returns>
private static char[] BytesToChars(Formatter formatter, PseudoOpNames opNames,
FormatDescriptor.SubType subType, byte[] data, int offset, int length,
out string popcode, out int showHexZeroes) {
Debug.Assert(length > 0);
// See also GenMerlin32.OutputString().
int strOffset = offset;
int strLen = length;
bool highAscii = false;
bool reverse = false;
showHexZeroes = 0;
switch (subType) {
case FormatDescriptor.SubType.None:
// High or low ASCII, full width specified by formatter.
highAscii = (data[offset] & 0x80) != 0;
popcode = highAscii ? opNames.StrGenericHi : opNames.StrGeneric;
break;
case FormatDescriptor.SubType.Dci:
// High or low ASCII, full width specified by formatter.
highAscii = (data[offset] & 0x80) != 0;
popcode = highAscii ? opNames.StrDciHi : opNames.StrDci;
break;
case FormatDescriptor.SubType.Reverse:
// High or low ASCII, full width specified by formatter. Show characters
// in reverse order.
highAscii = (data[offset + strLen - 1] & 0x80) != 0;
popcode = highAscii ? opNames.StrReverseHi : opNames.StrReverse;
reverse = true;
break;
case FormatDescriptor.SubType.DciReverse:
// High or low ASCII, full width specified by formatter. Show characters
// in reverse order.
highAscii = (data[offset + strLen - 1] & 0x80) != 0;
popcode = highAscii ? opNames.StrDciReverseHi : opNames.StrDciReverse;
reverse = true;
break;
case FormatDescriptor.SubType.CString:
// High or low ASCII, with a terminating null. Don't show the null. If
// it's an empty string, just show the null byte as hex.
highAscii = (data[offset] & 0x80) != 0;
popcode = highAscii ? opNames.StrNullTermHi : opNames.StrNullTerm;
strLen--;
if (strLen == 0) {
showHexZeroes = 1;
}
break;
case FormatDescriptor.SubType.L8String:
// High or low ASCII, with a leading length byte. Don't show the null.
// If it's an empty string, just show the length byte as hex.
strOffset++;
strLen--;
if (strLen == 0) {
showHexZeroes = 1;
} else {
highAscii = (data[strOffset] & 0x80) != 0;
}
popcode = highAscii ? opNames.StrLen8Hi : opNames.StrLen8;
break;
case FormatDescriptor.SubType.L16String:
// High or low ASCII, with a leading length word. Don't show the null.
// If it's an empty string, just show the length word as hex.
Debug.Assert(strLen > 1);
strOffset += 2;
strLen -= 2;
if (strLen == 0) {
showHexZeroes = 2;
} else {
highAscii = (data[strOffset] & 0x80) != 0;
}
popcode = highAscii ? opNames.StrLen16Hi : opNames.StrLen16;
break;
default:
Debug.Assert(false);
popcode = ".!!!";
break;
}
char[] text = new char[strLen];
if (!reverse) {
for (int i = 0; i < strLen; i++) {
text[i] = (char)(data[i + strOffset] & 0x7f);
}
} else {
for (int i = 0; i < strLen; i++) {
text[i] = (char)(data[strOffset + (strLen - i - 1)] & 0x7f);
}
}
return text;
}
/// <summary>
/// Generate multiple operand lines from a text line, adding optional delimiters.
/// </summary>
/// <param name="text">Buffer of characters to output. Must be ASCII.</param>
/// <param name="startDelim">Delimiter character(s), or the empty string.</param>
/// <param name="endDelim">Delimiter character(s), or the empty string.</param>
/// <param name="template">PseudoOut with offset, length, and opcode set. Each
/// returned PseudoOut will have these value plus the generated operand.</param>
/// <param name="outList">List that receives the generated items.</param>
private static void GenerateTextLines(char[] text, string startDelim, string endDelim,
PseudoOut template, List<PseudoOut> outList) {
// Could get fancy and break long strings at word boundaries.
int textOffset = 0;
if (text.Length == 0) {
// empty string
PseudoOut po = new PseudoOut(template);
po.Operand = startDelim + endDelim;
outList.Add(po);
return;
}
int textPerLine = MAX_OPERAND_LEN - (startDelim.Length + endDelim.Length);
StringBuilder sb = new StringBuilder(MAX_OPERAND_LEN);
while (textOffset < text.Length) {
int len = (text.Length - textOffset < textPerLine) ?
text.Length - textOffset : textPerLine;
sb.Clear();
sb.Append(startDelim);
sb.Append(new string(text, textOffset, len));
sb.Append(endDelim);
PseudoOut po = new PseudoOut(template);
po.Operand = sb.ToString();
outList.Add(po);
textOffset += len;
}
}
/// <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
}
/// <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) {
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:
return formatter.FormatAsciiOrHex(operandValue);
case FormatDescriptor.SubType.Symbol:
if (symbolTable.TryGetValue(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:
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 (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));
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View File

@ -0,0 +1,38 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:SourceGenWPF.Res">
<system:String x:Key="str_ErrBadFdFmt">Bad format descriptor at +{0:x6}.</system:String>
<system:String x:Key="str_ErrBadFdType">Bad format descriptor type</system:String>
<system:String x:Key="str_ErrBadFileLength">Bad file length</system:String>
<system:String x:Key="str_ErrBadIdent">Invalid file identifier</system:String>
<system:String x:Key="str_ErrBadRange">Bad range</system:String>
<system:String x:Key="str_ErrBadSymbolSt">Unknown Source or Type in symbol</system:String>
<system:String x:Key="str_ErrBadSymrefPart">Bad symbol reference part</system:String>
<system:String x:Key="str_ErrBadTypeHint">Type hint not recognized</system:String>
<system:String x:Key="str_ErrDuplicateLabelFmt">Removing duplicate label '{0}' (offset +{1:x6})</system:String>
<system:String x:Key="str_ErrFileExistsNotDirFmt">The file {0} exists, but is not a directory.</system:String>
<system:String x:Key="str_ErrFileNotFoundFmt">File not found: {0}</system:String>
<system:String x:Key="str_ErrFileReadOnlyFmt">Cannot write to read-only file {0}.</system:String>
<system:String x:Key="str_ErrInvalidIntValue">Could not convert value to integer</system:String>
<system:String x:Key="str_ErrInvalidKeyValue">Key value is out of range</system:String>
<system:String x:Key="str_ErrNotProjectFile">This does not appear to be a valid .dis65 project file</system:String>
<system:String x:Key="str_ErrProjectFileCorrupt">Project file may be corrupt</system:String>
<system:String x:Key="str_ErrProjectLoadFail">Unable to load project file</system:String>
<system:String x:Key="str_FileFilterCs">C# Source Files(*.cs)|*.cs</system:String>
<system:String x:Key="str_FileFilterDis65">SourceGen projects(*.dis65)|*.dis65</system:String>
<system:String x:Key="str_FileFilterSym65">SourceGen symbols (*.sym65)|*.sym65</system:String>
<system:String x:Key="str_InitialExtensionScripts">Extension scripts:</system:String>
<system:String x:Key="str_InitialParameters">Default settings:</system:String>
<system:String x:Key="str_InitialSymbolFiles">Symbol files:</system:String>
<system:String x:Key="str_ProjectFieldComment">comment</system:String>
<system:String x:Key="str_ProjectFieldLongComment">long comment</system:String>
<system:String x:Key="str_ProjectFieldNote">note</system:String>
<system:String x:Key="str_ProjectFieldOperandFormat">operand format</system:String>
<system:String x:Key="str_ProjectFieldStatusFlags">status flag override</system:String>
<system:String x:Key="str_ProjectFieldTypeHint">type hint</system:String>
<system:String x:Key="str_ProjectFieldUserLabel">user-defined label</system:String>
<system:String x:Key="str_ProjectFromNewerApp">This project was created by a newer version of SourceGen. It may contain data that will be lost if the project is edited.</system:String>
<system:String x:Key="str_SetupSystemSummaryFmt">{1} CPU @ {2} MHz</system:String>
</ResourceDictionary>

View File

@ -0,0 +1,71 @@
using System;
using System.Windows;
namespace SourceGenWPF.Res {
public static class Strings {
public static string ERR_BAD_FD_FMT =
(string)Application.Current.FindResource("str_ErrBadFdFmt");
public static string ERR_BAD_FD_TYPE =
(string)Application.Current.FindResource("str_ErrBadFdType");
public static string ERR_BAD_FILE_LENGTH =
(string)Application.Current.FindResource("str_ErrBadFileLength");
public static string ERR_BAD_IDENT =
(string)Application.Current.FindResource("str_ErrBadIdent");
public static string ERR_BAD_RANGE =
(string)Application.Current.FindResource("str_ErrBadRange");
public static string ERR_BAD_SYMBOL_ST =
(string)Application.Current.FindResource("str_ErrBadSymbolSt");
public static string ERR_BAD_SYMREF_PART =
(string)Application.Current.FindResource("str_ErrBadSymrefPart");
public static string ERR_BAD_TYPE_HINT =
(string)Application.Current.FindResource("str_ErrBadTypeHint");
public static string ERR_DUPLICATE_LABEL_FMT =
(string)Application.Current.FindResource("str_ErrDuplicateLabelFmt");
public static string ERR_FILE_EXISTS_NOT_DIR_FMT =
(string)Application.Current.FindResource("str_ErrFileExistsNotDirFmt");
public static string ERR_FILE_NOT_FOUND_FMT =
(string)Application.Current.FindResource("str_ErrFileNotFoundFmt");
public static string ERR_FILE_READ_ONLY_FMT =
(string)Application.Current.FindResource("str_ErrFileReadOnlyFmt");
public static string ERR_INVALID_INT_VALUE =
(string)Application.Current.FindResource("str_ErrInvalidIntValue");
public static string ERR_INVALID_KEY_VALUE =
(string)Application.Current.FindResource("str_ErrInvalidKeyValue");
public static string ERR_NOT_PROJECT_FILE =
(string)Application.Current.FindResource("str_ErrNotProjectFile");
public static string ERR_PROJECT_FILE_CORRUPT =
(string)Application.Current.FindResource("str_ErrProjectFileCorrupt");
public static string ERR_PROJECT_LOAD_FAIL =
(string)Application.Current.FindResource("str_ErrProjectLoadFail");
public static string FILE_FILTER_CS =
(string)Application.Current.FindResource("str_FileFilterCs");
public static string FILE_FILTER_DIS65 =
(string)Application.Current.FindResource("str_FileFilterDis65");
public static string FILE_FILTER_SYM65 =
(string)Application.Current.FindResource("str_FileFilterSym65");
public static string INITIAL_EXTENSION_SCRIPTS =
(string)Application.Current.FindResource("str_InitialExtensionScripts");
public static string INITIAL_PARAMETERS =
(string)Application.Current.FindResource("str_InitialParameters");
public static string INITIAL_SYMBOL_FILES =
(string)Application.Current.FindResource("str_InitialSymbolFiles");
public static string PROJECT_FIELD_COMMENT =
(string)Application.Current.FindResource("str_ProjectFieldComment");
public static string PROJECT_FIELD_LONG_COMMENT =
(string)Application.Current.FindResource("str_ProjectFieldLongComment");
public static string PROJECT_FIELD_NOTE =
(string)Application.Current.FindResource("str_ProjectFieldNote");
public static string PROJECT_FIELD_OPERAND_FORMAT =
(string)Application.Current.FindResource("str_ProjectFieldOperandFormat");
public static string PROJECT_FIELD_STATUS_FLAGS =
(string)Application.Current.FindResource("str_ProjectFieldStatusFlags");
public static string PROJECT_FIELD_TYPE_HINT =
(string)Application.Current.FindResource("str_ProjectFieldTypeHint");
public static string PROJECT_FIELD_USER_LABEL =
(string)Application.Current.FindResource("str_ProjectFieldUserLabel");
public static string PROJECT_FROM_NEWER_APP =
(string)Application.Current.FindResource("str_ProjectFromNewerApp");
public static string SETUP_SYSTEM_SUMMARY_FMT =
(string)Application.Current.FindResource("str_SetupSystemSummaryFmt");
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Diagnostics;
using System.IO;
namespace SourceGenWPF {
/// <summary>
/// Facilitates access to the contents of the RuntimeData directory, which is located
/// relative to the executable process pathname.
/// </summary>
public static class RuntimeDataAccess {
private const string RUNTIME_DATA_FILENAME = "RuntimeData";
private static string sBasePath;
/// <summary>
/// Attempts to locate the RuntimeData directory. This will normally live in the
/// place the executable starts from, but if we're debugging in Visual Studio then
/// it'll be up a couple levels (e.g. from "bin/Release").
/// </summary>
/// <returns>Full path of the RuntimeData directory, or null on failure.</returns>
private static string FindBasePath() {
if (sBasePath != null) {
return sBasePath;
}
// Process.GetCurrentProcess().MainModule.FileName returns "/usr/bin/mono-sgen"
// under Linux, which is not what we want. Since this class is part of the main
// executable, we can use our own assembly location to get the desired answer.
string exeName = typeof(RuntimeDataAccess).Assembly.Location;
string baseDir = Path.GetDirectoryName(exeName);
if (string.IsNullOrEmpty(baseDir)) {
return null;
}
string tryPath;
tryPath = Path.Combine(baseDir, RUNTIME_DATA_FILENAME);
if (Directory.Exists(tryPath)) {
sBasePath = Path.GetFullPath(tryPath);
return sBasePath;
}
string upTwo = Path.GetDirectoryName(Path.GetDirectoryName(baseDir));
tryPath = Path.Combine(upTwo, RUNTIME_DATA_FILENAME);
if (Directory.Exists(tryPath)) {
sBasePath = Path.GetFullPath(tryPath);
return sBasePath;
}
Debug.WriteLine("Unable to find RuntimeData dir near " + exeName);
return null;
}
/// <summary>
/// Returns the full path of the runtime data directory.
/// </summary>
/// <returns>Full path name, or null if the base path can't be found.</returns>
public static string GetDirectory() {
return FindBasePath();
}
/// <summary>
/// Returns a full path, prefixing the file name with the base path name.
/// </summary>
/// <param name="fileName">Relative file name.</param>
/// <returns>Full path name, or null if the base path can't be found.</returns>
public static string GetPathName(string fileName) {
string basePath = FindBasePath();
if (basePath == null) {
return null;
}
// Combine() joins "C:\foo" and "bar/ack" into "C:\foo\bar/ack", which works, but
// looks funny. GetFullPath() normalizes the directory separators. The file
// isn't required to exist, but if it does, path information must be available.
// Given the nature of this class, that shouldn't be limiting.
return Path.GetFullPath(Path.Combine(basePath, fileName));
}
/// <summary>
/// Given the pathname of a file in the RuntimeData directory, strip off the
/// directory.
/// </summary>
/// <param name="fullPath">Absolute pathname of file. Assumed to be in canonical
/// form.</param>
/// <returns>Partial path within the runtime data directory.</returns>
public static string PartialPath(string fullPath) {
string basePath = FindBasePath();
if (basePath == null) {
return null;
}
basePath += Path.DirectorySeparatorChar;
if (!fullPath.StartsWith(basePath)) {
return null;
}
return fullPath.Substring(basePath.Length);
}
}
}

View File

@ -0,0 +1,255 @@
/*
* 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.Runtime.Remoting.Lifetime;
using System.Security;
using System.Security.Permissions;
using System.Timers;
using PluginCommon;
namespace SourceGenWPF.Sandbox {
/// <summary>
/// This is a host-side object that manages the plugin AppDomain.
/// </summary>
//[SecurityPermission(SecurityAction.LinkDemand, ControlAppDomain = true, Infrastructure = true)]
public class DomainManager : IDisposable {
/// <summary>
/// For IDisposable.
/// </summary>
private bool mDisposed = false;
/// <summary>
/// AppDomain handle.
/// </summary>
private AppDomain mAppDomain;
/// <summary>
/// Reference to the remote PluginManager object.
/// </summary>
private Sponsor<PluginManager> mPluginManager;
/// <summary>
/// Hack to keep the sandbox from disappearing.
/// </summary>
private Timer mKeepAliveTimer;
/// <summary>
/// Access the remote PluginManager object.
/// </summary>
public PluginManager PluginMgr {
get {
Debug.Assert(mPluginManager.CheckLease());
return mPluginManager.Instance;
}
}
/// <summary>
/// App domain ID, or -1 if not available.
/// </summary>
public int Id { get { return mAppDomain != null ? mAppDomain.Id : -1; } }
public DomainManager(bool useKeepAlive) {
// Sometimes the sandbox AppDomain can't call back into the main AppDomain to
// get a lease renewal, and the PluginManager object gets collected. See
// https://stackoverflow.com/q/52230527/294248 for details.
//
// The idea is to keep tickling renew-on-call, so that the plugin side never
// has to request renewal. This is ugly but seems to work.
//
// The timer event runs on a pool thread, and calls across domains seem to stay
// on the same thread, so the remote Ping() method must be prepared to be called
// on an arbitrary thread.
if (useKeepAlive) {
Debug.WriteLine("Setting keep-alive timer...");
mKeepAliveTimer = new Timer(60 * 1000);
mKeepAliveTimer.Elapsed += (source, e) => {
// I don't know if there's a shutdown race. The dispose code stops the timer
// before clearing the other fields, but I don't know if the Stop() code
// waits for the currently-executing timer event to finish. So wrap
// everything in try/catch.
try {
mPluginManager.Instance.Ping(0);
//Debug.WriteLine("KeepAlive tid=" +
// System.Threading.Thread.CurrentThread.ManagedThreadId);
} catch (Exception ex) {
Debug.WriteLine("Keep-alive timer failed: " + ex.Message);
}
};
mKeepAliveTimer.AutoReset = true;
mKeepAliveTimer.Enabled = true;
}
}
/// <summary>
/// Creates a new AppDomain. If our plugin is just executing
/// pre-compiled code we can lock the permissions down, but if
/// it needs to dynamically compile code we need to open things up.
/// </summary>
/// <param name="appDomainName">The "friendly" name.</param>
/// <param name="appBaseBath">Directory to use for ApplicationBase.</param>
public void CreateDomain(string appDomainName, string appBaseBath) {
// This doesn't seem to affect Sponsor. Doing this over in the PluginManager
// does have the desired effect, but requires unrestricted security.
//LifetimeServices.LeaseTime = TimeSpan.FromSeconds(5);
//LifetimeServices.LeaseManagerPollTime = TimeSpan.FromSeconds(3);
//LifetimeServices.RenewOnCallTime = TimeSpan.FromSeconds(2);
//LifetimeServices.SponsorshipTimeout = TimeSpan.FromSeconds(1);
if (mAppDomain != null) {
throw new Exception("Domain already created");
}
PermissionSet permSet;
// Start with everything disabled.
permSet = new PermissionSet(PermissionState.None);
//permSet = new PermissionSet(PermissionState.Unrestricted);
// Allow code execution.
permSet.AddPermission(new SecurityPermission(
SecurityPermissionFlag.Execution));
// This appears to be necessary to allow the lease renewal to work. Without
// this the lease silently fails to renew.
permSet.AddPermission(new SecurityPermission(
SecurityPermissionFlag.Infrastructure));
// Allow changes to Remoting stuff. Without this, we can't
// register our ISponsor.
permSet.AddPermission(new SecurityPermission(
SecurityPermissionFlag.RemotingConfiguration));
// Allow read-only file access, but only in the plugin directory.
// This is necessary to allow PluginLoader to load the assembly.
FileIOPermission fp = new FileIOPermission(
FileIOPermissionAccess.Read | FileIOPermissionAccess.PathDiscovery,
appBaseBath);
permSet.AddPermission(fp);
// TODO(maybe): it looks like this would allow us to mark the PluginCommon dll as
// trusted, so we wouldn't have to give the above permissions to everything.
// That seems to require a cryptographic pair and some other voodoo.
//StrongName fullTrustAssembly =
// typeof(PluginManager).Assembly.Evidence.GetHostEvidence<StrongName>();
// Configure the AppDomain. Setting the ApplicationBase directory away from
// the main app location is apparently very important, as it mitigates the
// risk of certain exploits from untrusted plugin code.
AppDomainSetup adSetup = new AppDomainSetup();
adSetup.ApplicationBase = appBaseBath;
// Create the AppDomain.
mAppDomain = AppDomain.CreateDomain(appDomainName, null, adSetup, permSet);
Debug.WriteLine("Created AppDomain '" + appDomainName + "', id=" + mAppDomain.Id);
//Debug.WriteLine("Loading '" + typeof(PluginManager).Assembly.FullName + "' / '" +
// typeof(PluginManager).FullName + "'");
// Create a PluginManager in the remote AppDomain. The local
// object is actually a proxy.
PluginManager pm = (PluginManager)mAppDomain.CreateInstanceAndUnwrap(
typeof(PluginManager).Assembly.FullName,
typeof(PluginManager).FullName);
// Wrap it so it doesn't disappear on us.
mPluginManager = new Sponsor<PluginManager>(pm);
Debug.WriteLine("IsTransparentProxy: " +
System.Runtime.Remoting.RemotingServices.IsTransparentProxy(pm));
}
/// <summary>
/// Destroy the AppDomain.
/// </summary>
private void DestroyDomain(bool disposing) {
Debug.WriteLine("Unloading AppDomain '" + mAppDomain.FriendlyName +
"', id=" + mAppDomain.Id + ", disposing=" + disposing);
if (mKeepAliveTimer != null) {
mKeepAliveTimer.Stop();
mKeepAliveTimer.Dispose();
mKeepAliveTimer = null;
}
if (mPluginManager != null) {
mPluginManager.Dispose();
mPluginManager = null;
}
if (mAppDomain != null) {
// We can't simply invoke AppDomain.Unload() from a finalizer. The unload is
// handled by a thread that won't run at the same time as the finalizer thread,
// so if we got here through finalization we will deadlock. Fortunately the
// runtime sees the situation and throws an exception out of Unload().
//
// If we don't have a finalizer, and we forget to make an explicit cleanup
// call, the AppDomain will stick around and keep the DLL files locked, which
// could be annoying if the user is trying to iterate on extension script
// development.
//
// So we use a workaround from https://stackoverflow.com/q/4064749/294248
// and invoke it asynchronously.
if (disposing) {
AppDomain.Unload(mAppDomain);
} else {
new Action<AppDomain>(AppDomain.Unload).BeginInvoke(mAppDomain, null, null);
}
mAppDomain = null;
}
}
/// <summary>
/// Finalizer. Required for IDisposable.
/// </summary>
~DomainManager() {
Debug.WriteLine("WARNING: DomainManager finalizer running (id=" +
(mAppDomain != null ? mAppDomain.Id.ToString() : "--") + ")");
Dispose(false);
}
/// <summary>
/// Generic IDisposable implementation.
/// </summary>
public void Dispose() {
// Dispose of unmanaged resources (i.e. the AppDomain).
Dispose(true);
// Suppress finalization.
GC.SuppressFinalize(this);
}
/// <summary>
/// Destroys the AppDomain, if one was created.
/// </summary>
/// <param name="disposing">True if called from Dispose(), false if from finalizer.</param>
protected virtual void Dispose(bool disposing) {
if (mDisposed) {
return;
}
if (disposing) {
// Free *managed* objects here. This is mostly an
// optimization, as such things will be disposed of
// eventually by the GC.
}
// Free unmanaged objects (i.e. the AppDomain).
if (mAppDomain != null) {
DestroyDomain(disposing);
}
mDisposed = true;
}
}
}

View File

@ -0,0 +1,237 @@
/*
* 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.CodeDom.Compiler;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using CommonUtil;
using PluginCommon;
namespace SourceGenWPF.Sandbox {
/// <summary>
/// This manages the PluginDll directory, which holds the compiled form of the extension
/// scripts. When a script is requested, this checks to see if the compiled form
/// already exists. If not, or the script source file is newer than the DLL file, the
/// compiler is executed.
///
/// This is global -- it's not tied to an active project.
///
/// If an assembly is still loaded, the file on disk will be locked by the operating
/// system and can't be replaced. So long as the plugins run in an AppDomain sandbox,
/// the locks will be cleared when the AppDomain is unloaded.
/// </summary>
public static class PluginDllCache {
private const string PLUGIN_DIR_NAME = "PluginDll";
/// <summary>
/// List of assemblies for the CompilerParameters.ReferencedAssemblies argument.
/// </summary>
private static readonly string[] sRefAssem = new string[] {
// Need this for various things to work, like System.Collections.Generic.
"netstandard.dll",
// Plugins are implemented in terms of interfaces defined here.
"PluginCommon.dll",
// Common utility functions.
"CommonUtil.dll",
};
/// <summary>
/// Path to plugin directory.
/// </summary>
private static string sPluginDirPath;
/// <summary>
/// Computes the path to the plugin directory. Does not attempt to verify that it exists.
/// </summary>
/// <returns>Plugin directory path, or null if we can't find the application data
/// area.</returns>
public static string GetPluginDirPath() {
if (sPluginDirPath == null) {
string runtimeUp = Path.GetDirectoryName(RuntimeDataAccess.GetDirectory());
if (runtimeUp == null) {
return null;
}
sPluginDirPath = Path.Combine(runtimeUp, PLUGIN_DIR_NAME);
}
return sPluginDirPath;
}
/// <summary>
/// Prepares the plugin directory. Creates it and copies PluginCommon.dll in.
/// Throws an exception if something fails.
/// </summary>
public static void PreparePluginDir() {
string dstDir = GetPluginDirPath();
if (File.Exists(dstDir) && !Directory.Exists(dstDir)) {
throw new IOException(
string.Format(Res.Strings.ERR_FILE_EXISTS_NOT_DIR_FMT, dstDir));
}
Directory.CreateDirectory(dstDir);
// TODO(someday): try to remove *.dll where the modification date is more than a
// week old -- this will prevent us from accreting stuff indefinitely.
// Copy PluginCommon and CommonUtil over.
CopyIfNewer(typeof(PluginCommon.PluginManager).Assembly.Location, dstDir);
CopyIfNewer(typeof(CommonUtil.CRC32).Assembly.Location, dstDir);
}
/// <summary>
/// Copies a DLL file if it's not present in the destination directory, or
/// if it's newer than what's in the destination directory.
/// </summary>
/// <param name="srcDll">Full path to DLL file.</param>
/// <param name="dstDir">Destination directory.</param>
private static void CopyIfNewer(string srcDll, string dstDir) {
string dstFile = Path.Combine(dstDir, Path.GetFileName(srcDll));
if (FileUtil.FileMissingOrOlder(dstFile, srcDll)) {
Debug.WriteLine("Copying " + srcDll + " to " + dstFile);
File.Copy(srcDll, dstFile, true);
}
// Should we copy the .pdb files too, if they exist? If they don't exist in
// the source directory, do we need to remove them from the destination directory?
}
/// <summary>
/// Prepares the DLL for the specified script, compiling it if necessary.
/// </summary>
/// <param name="scriptIdent">Script identifier.</param>
/// <param name="projectPathName">Project file name, used for naming project-local
/// files. May be empty if the project hasn't been named yet (in which case
/// project-local files will cause a failure).</param>
/// <param name="report">Report with errors and warnings.</param>
/// <returns>Full path to DLL, or null if compilation failed.</returns>
public static string GenerateScriptDll(string scriptIdent, string projectPathName,
out FileLoadReport report) {
ExternalFile ef = ExternalFile.CreateFromIdent(scriptIdent);
if (ef == null) {
Debug.Assert(false);
report = new FileLoadReport("CreateFromIdent failed");
return null;
}
string projectDir = string.Empty;
if (!string.IsNullOrEmpty(projectPathName)) {
projectDir = Path.GetDirectoryName(projectPathName);
}
string srcPathName = ef.GetPathName(projectDir);
// Fail if the source script doesn't exist. If a previously-compiled DLL is present
// we could just continue to use it, but that seems contrary to expectation, and
// means that you won't notice that your project is broken until you clear out
// the DLL directory.
if (!File.Exists(srcPathName)) {
report = new FileLoadReport(srcPathName);
report.Add(FileLoadItem.Type.Error,
string.Format(Res.Strings.ERR_FILE_NOT_FOUND_FMT, srcPathName));
return null;
}
string destFileName = ef.GenerateDllName(projectPathName);
string destPathName = Path.Combine(GetPluginDirPath(), destFileName);
// Compile if necessary.
if (FileUtil.FileMissingOrOlder(destPathName, srcPathName)) {
Debug.WriteLine("Compiling " + srcPathName + " to " + destPathName);
Assembly asm = CompileCode(srcPathName, destPathName, out report);
if (asm == null) {
return null;
}
} else {
Debug.WriteLine("NOT recompiling " + srcPathName);
report = new FileLoadReport(srcPathName);
}
return destPathName;
}
/// <summary>
/// Compiles the script from the specified pathname into an Assembly.
/// </summary>
/// <param name="scriptPathName">Script pathname.</param>
/// <param name="dllPathName">Full pathname for output DLL.</param>
/// <param name="report">Errors and warnings reported by the compiler.</param>
/// <returns>Reference to script instance, or null on failure.</returns>
private static Assembly CompileCode(string scriptPathName, string dllPathName,
out FileLoadReport report) {
report = new FileLoadReport(scriptPathName);
Microsoft.CSharp.CSharpCodeProvider csProvider =
new Microsoft.CSharp.CSharpCodeProvider();
CompilerParameters parms = new CompilerParameters();
// We want a DLL, not an EXE.
parms.GenerateExecutable = false;
// Save to disk so other AppDomain can load it.
parms.GenerateInMemory = false;
// Be vocal about warnings.
parms.WarningLevel = 3;
// Optimization is nice.
parms.CompilerOptions = "/optimize";
// Output file name. Must be named appropriately so it can be found.
parms.OutputAssembly = dllPathName;
// Add dependencies.
parms.ReferencedAssemblies.AddRange(sRefAssem);
#if DEBUG
// This creates a .pdb file, which allows breakpoints to work.
parms.IncludeDebugInformation = true;
#endif
// Using the "from file" version has an advantage over the "from source"
// version in that the debugger can find the source file, so things like
// breakpoints work correctly.
CompilerResults cr = csProvider.CompileAssemblyFromFile(parms, scriptPathName);
CompilerErrorCollection cec = cr.Errors;
foreach (CompilerError ce in cr.Errors) {
report.Add(ce.Line, ce.Column,
ce.IsWarning ? FileLoadItem.Type.Warning : FileLoadItem.Type.Error,
ce.ErrorText);
}
if (cr.Errors.HasErrors) {
return null;
} else {
Debug.WriteLine("Compilation successful");
return cr.CompiledAssembly;
}
}
/// <summary>
/// Finds the first concrete class that implements IPlugin, and
/// constructs an instance.
/// </summary>
public static IPlugin ConstructIPlugin(Assembly asm) {
foreach (Type type in asm.GetExportedTypes()) {
// Using a System.Linq extension method.
if (type.IsClass && !type.IsAbstract &&
type.GetInterfaces().Contains(typeof(IPlugin))) {
ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
IPlugin iplugin = (IPlugin)ctor.Invoke(null);
Debug.WriteLine("Created instance: " + iplugin);
return iplugin;
}
}
return null;
}
}
}

View File

@ -0,0 +1,226 @@
/*
* 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 CommonUtil;
using PluginCommon;
namespace SourceGenWPF.Sandbox {
/// <summary>
/// Maintains a collection of IPlugin instances, or communicates with the remote
/// PluginManager that holds the collection. Whether the plugins are instantiated
/// locally depends on how the class is constructed.
/// </summary>
public class ScriptManager {
public const string FILENAME_EXT = ".cs";
public static readonly string FILENAME_FILTER = Res.Strings.FILE_FILTER_CS;
/// <summary>
/// If true, the DomainManager will use the keep-alive timer hack.
/// </summary>
public static bool UseKeepAliveHack { get; set; }
/// <summary>
/// Reference to DomainManager, if we're using one.
/// </summary>
public DomainManager DomainMgr { get; private set; }
/// <summary>
/// Collection of loaded plugins, if we're not using a DomainManager.
/// </summary>
private Dictionary<string, IPlugin> mActivePlugins;
/// <summary>
/// Reference to project, from which we can get the file data and project path name.
/// </summary>
private DisasmProject mProject;
/// <summary>
/// Constructor.
/// </summary>
public ScriptManager(DisasmProject proj) {
mProject = proj;
if (!proj.UseMainAppDomainForPlugins) {
DomainMgr = new DomainManager(UseKeepAliveHack);
DomainMgr.CreateDomain("Plugin Domain", PluginDllCache.GetPluginDirPath());
DomainMgr.PluginMgr.SetFileData(proj.FileData);
} else {
mActivePlugins = new Dictionary<string, IPlugin>();
}
}
/// <summary>
/// Cleans up, discarding the AppDomain if one was created. Do not continue to use
/// the object after calling this.
/// </summary>
public void Cleanup() {
if (DomainMgr != null) {
DomainMgr.Dispose();
DomainMgr = null;
}
mActivePlugins = null;
mProject = null;
}
/// <summary>
/// Clears the list of plugins. This does not unload assemblies. Call this when
/// the list of extension scripts configured into the project has changed.
/// </summary>
public void Clear() {
if (DomainMgr == null) {
mActivePlugins.Clear();
} else {
DomainMgr.PluginMgr.ClearPluginList();
}
}
/// <summary>
/// Attempts to load the specified plugin. If the plugin is already loaded, this
/// does nothing. If not, the assembly is loaded and an instance is created.
/// </summary>
/// <param name="scriptIdent">Script identifier.</param>
/// <param name="report">Report with errors and warnings.</param>
/// <returns>True on success.</returns>
public bool LoadPlugin(string scriptIdent, out FileLoadReport report) {
// Make sure the most recent version is compiled.
string dllPath = PluginDllCache.GenerateScriptDll(scriptIdent,
mProject.ProjectPathName, out report);
if (dllPath == null) {
return false;
}
if (DomainMgr == null) {
if (mActivePlugins.TryGetValue(scriptIdent, out IPlugin plugin)) {
return true;
}
Assembly asm = Assembly.LoadFile(dllPath);
plugin = PluginDllCache.ConstructIPlugin(asm);
mActivePlugins.Add(scriptIdent, plugin);
report = new FileLoadReport(dllPath); // empty report
return true;
} else {
IPlugin plugin = DomainMgr.PluginMgr.LoadPlugin(dllPath, scriptIdent);
return plugin != null;
}
}
public IPlugin GetInstance(string scriptIdent) {
if (DomainMgr == null) {
if (mActivePlugins.TryGetValue(scriptIdent, out IPlugin plugin)) {
return plugin;
}
Debug.Assert(false);
return null;
} else {
return DomainMgr.PluginMgr.GetPlugin(scriptIdent);
}
}
/// <summary>
/// Generates a list of references to instances of loaded plugins.
/// </summary>
/// <returns>Newly-created list of plugin references.</returns>
public List<IPlugin> GetAllInstances() {
if (DomainMgr == null) {
List<IPlugin> list = new List<IPlugin>(mActivePlugins.Count);
foreach (KeyValuePair<string, IPlugin> kvp in mActivePlugins) {
list.Add(kvp.Value);
}
return list;
} else {
return DomainMgr.PluginMgr.GetActivePlugins();
}
}
/// <summary>
/// Prepares all active scripts for action.
/// </summary>
/// <param name="appRef">Reference to object providing app services.</param>
public void PrepareScripts(IApplication appRef) {
List<PlatSym> platSyms = GeneratePlatSymList();
if (DomainMgr == null) {
foreach (KeyValuePair<string, IPlugin> kvp in mActivePlugins) {
kvp.Value.Prepare(appRef, mProject.FileData, platSyms);
}
} else {
DomainMgr.PluginMgr.PreparePlugins(appRef, platSyms);
}
}
/// <summary>
/// Gathers a list of platform symbols from the project's symbol table.
/// </summary>
private List<PlatSym> GeneratePlatSymList() {
List<PlatSym> platSyms = new List<PlatSym>();
SymbolTable symTab = mProject.SymbolTable;
foreach (Symbol sym in symTab) {
if (!(sym is DefSymbol)) {
// ignore user labels
continue;
}
DefSymbol defSym = sym as DefSymbol;
if (defSym.SymbolSource != Symbol.Source.Platform) {
// ignore project symbols
continue;
}
platSyms.Add(new PlatSym(defSym.Label, defSym.Value, defSym.Tag));
}
return platSyms;
}
/// <summary>
/// For debugging purposes, get some information about the currently loaded
/// extension scripts.
/// </summary>
public string DebugGetLoadedScriptInfo() {
StringBuilder sb = new StringBuilder();
if (DomainMgr == null) {
foreach (KeyValuePair<string, IPlugin> kvp in mActivePlugins) {
string loc = kvp.Value.GetType().Assembly.Location;
sb.Append("[main] ");
sb.Append(loc);
sb.Append("\r\n ");
DebugGetScriptInfo(kvp.Value, sb);
}
} else {
List<IPlugin> plugins = DomainMgr.PluginMgr.GetActivePlugins();
foreach (IPlugin plugin in plugins) {
string loc = DomainMgr.PluginMgr.GetPluginAssemblyLocation(plugin);
sb.AppendFormat("[sub {0}] ", DomainMgr.Id);
sb.Append(loc);
sb.Append("\r\n ");
DebugGetScriptInfo(plugin, sb);
}
}
return sb.ToString();
}
private void DebugGetScriptInfo(IPlugin plugin, StringBuilder sb) {
sb.Append(plugin.Identifier);
sb.Append("\r\n");
}
}
}

View File

@ -0,0 +1,177 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Diagnostics;
using System.Runtime.Remoting.Lifetime;
using System.Security.Permissions;
namespace SourceGenWPF.Sandbox {
/// <summary>
/// This wraps a MarshalByRefObject instance with a "sponsor". This
/// is necessary because objects created by the host in the plugin
/// AppDomain aren't strongly referenced across the boundary (the two
/// AppDomains have independent garbage collection). Because the plugin
/// AppDomain can't know when the host AppDomain discards its objects,
/// it will discard remote-proxied objects on its side after a period of disuse.
///
/// The ISponsor/ILease mechanism provides a way for the host-side object
/// to define the lifespan of the plugin-side objects. The object
/// manager in the plugin AppDomain will invoke Renewal() back in the host-side
/// AppDomain.
/// </summary>
[SecurityPermission(SecurityAction.Demand, Infrastructure = true)]
class Sponsor<T> : MarshalByRefObject, ISponsor, IDisposable where T : MarshalByRefObject {
/// <summary>
/// The object we've wrapped.
/// </summary>
private T mObj;
/// <summary>
/// For IDisposable.
/// </summary>
private bool mDisposed = false;
// For debugging, track the last renewal time.
private DateTime mLastRenewal = DateTime.Now;
public T Instance {
get {
if (mDisposed) {
throw new ObjectDisposedException("Sponsor was disposed");
} else {
return mObj;
}
}
}
public Sponsor(T obj) {
mObj = obj;
// Get the lifetime service lease from the MarshalByRefObject,
// and register ourselves as a sponsor.
ILease lease = (ILease)obj.GetLifetimeService();
lease.Register(this);
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "|Sponsor created; initLt=" +
lease.InitialLeaseTime + " renOC=" + lease.RenewOnCallTime +
" spon=" + lease.SponsorshipTimeout);
}
public bool CheckLease() {
try {
ILease lease = (ILease)mObj.GetLifetimeService();
if (lease.CurrentState != LeaseState.Active) {
Debug.WriteLine("WARNING: lease has expired for " + mObj);
return false;
}
} catch (System.Runtime.Remoting.RemotingException ex) {
Debug.WriteLine("WARNING: remote object gone: " + ex.Message);
return false;
}
return true;
}
/// <summary>
/// Extends the lease time for the wrapped object. This is called
/// from the plugin AppDomain, but executes on the host AppDomain.
/// </summary>
[SecurityPermissionAttribute(SecurityAction.LinkDemand,
Flags = SecurityPermissionFlag.Infrastructure)]
TimeSpan ISponsor.Renewal(ILease lease) {
DateTime now = DateTime.Now;
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss") + "|Lease renewal for " + mObj +
", last renewed " + (now - mLastRenewal) + " sec ago; renewing for " +
lease.RenewOnCallTime + " (host id=" + AppDomain.CurrentDomain.Id + ")");
mLastRenewal = now;
if (mDisposed) {
// Shouldn't happen -- we should be unregistered -- but I
// don't know if multiple threads are involved.
Debug.WriteLine("WARNING: attempted to renew a disposed Sponsor");
return TimeSpan.Zero;
} else {
// Use the lease's RenewOnCallTime.
return lease.RenewOnCallTime;
}
}
/// <summary>
/// Finalizer. Required for IDisposable.
/// </summary>
~Sponsor() {
Dispose(false);
}
/// <summary>
/// Generic IDisposable implementation.
/// </summary>
public void Dispose() {
// Dispose of unmanaged resources.
Dispose(true);
// Suppress finalization.
GC.SuppressFinalize(this);
}
/// <summary>
/// Destroys the Sponsor, if one was created.
/// </summary>
/// <param name="disposing">True if called from Dispose(), false if from finalizer.</param>
protected virtual void Dispose(bool disposing) {
if (mDisposed) {
return;
}
Debug.WriteLine("Sponsor.Dispose(disposing=" + disposing + ")");
// If this is a managed object, call its Dispose method.
if (disposing) {
if (mObj is IDisposable) {
((IDisposable)mObj).Dispose();
}
}
// Remove ourselves from the lifetime service.
// NOTE: if you see this blowing up at app shutdown, it's because you didn't
// call Dispose() on the DomainManager.
object leaseObj;
try {
leaseObj = mObj.GetLifetimeService();
} catch (Exception ex) {
// This seems to happen when we shut down without having disposed of the
// AppDomain, probably when a Sponsor's finalizer runs before the
// DomainManager's finalizer. Sometimes it also happens when you seem to
// be doing everything right, though this seems to correspond with a lack
// of lease renewal messages (i.e. something is really wrong as the other end).
//
// I think failures here can be ignored, since it's just failure to clean up
// something that doesn't exist.
//
// Sometimes it's:
// RemotingException: Object '---' has been disconnected or does not exist at the server.
Debug.WriteLine("WARNING: GetLifetimeService failed: " + ex.Message);
leaseObj = null;
}
if (leaseObj is ILease) {
ILease lease = (ILease)leaseObj;
lease.Unregister(this);
}
mDisposed = true;
}
}
}

View File

@ -37,6 +37,7 @@
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Web.Extensions" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
@ -55,12 +56,45 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="AddressMap.cs" />
<Compile Include="Anattrib.cs" />
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="PseudoOp.cs" />
<Compile Include="Res\Strings.xaml.cs" />
<Compile Include="RuntimeDataAccess.cs" />
<Compile Include="Sandbox\DomainManager.cs" />
<Compile Include="Sandbox\PluginDllCache.cs" />
<Compile Include="Sandbox\ScriptManager.cs" />
<Compile Include="Sandbox\Sponsor.cs" />
<Compile Include="Symbol.cs" />
<Compile Include="SymbolTable.cs" />
<Compile Include="SystemDefaults.cs" />
<Compile Include="SystemDefs.cs" />
<Compile Include="UndoableChange.cs" />
<Compile Include="VirtualListViewSelection.cs" />
<Compile Include="WeakSymbolRef.cs" />
<Compile Include="XrefSet.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="AppSettings.cs" />
<Compile Include="AutoLabel.cs" />
<Compile Include="ChangeSet.cs" />
<Compile Include="CodeAnalysis.cs" />
<Compile Include="DataAnalysis.cs" />
<Compile Include="DefSymbol.cs" />
<Compile Include="DisasmProject.cs" />
<Compile Include="DisplayList.cs" />
<Compile Include="ExternalFile.cs" />
<Compile Include="FormatDescriptor.cs" />
<Compile Include="HelpAccess.cs" />
<Compile Include="MultiLineComment.cs" />
<Compile Include="NavStack.cs" />
<Compile Include="PlatformSymbols.cs" />
<Compile Include="ProjectFile.cs" />
<Compile Include="ProjectProperties.cs" />
<Compile Include="ProjWin\MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
</Compile>
@ -97,6 +131,27 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Res\Strings.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Asm65\Asm65.csproj">
<Project>{65a50bd0-ab07-492b-b51c-4ca1b700224d}</Project>
<Name>Asm65</Name>
</ProjectReference>
<ProjectReference Include="..\CommonUtil\CommonUtil.csproj">
<Project>{a2993eac-35d8-4768-8c54-152b4e14d69c}</Project>
<Name>CommonUtil</Name>
</ProjectReference>
<ProjectReference Include="..\PluginCommon\PluginCommon.csproj">
<Project>{70f04543-9e46-4ad3-875a-160fd198c0ff}</Project>
<Name>PluginCommon</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Resource Include="Res\SourceGenIcon.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

156
SourceGenWPF/Symbol.cs Normal file
View File

@ -0,0 +1,156 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Diagnostics;
namespace SourceGenWPF {
/// <summary>
/// Symbolic representation of a value. Instances are immutable.
/// </summary>
public class Symbol {
/// <summary>
/// Was the symbol defined by the user, or generated automatically?
/// </summary>
public enum Source {
// These are in order of highest to lowest precedence. This matters when
// looking up a symbol by value, since multiple symbols can have the same value.
Unknown = 0,
User, // user-defined label
Project, // from project configuration file
Platform, // from platform definition file
Auto // auto-generated label
}
/// <summary>
/// Local internal label, global internal label, or reference to an
/// external address? Constants get a separate type in case we need to
/// distinguish them from addresses.
/// </summary>
public enum Type {
Unknown = 0,
LocalOrGlobalAddr, // local symbol, may be promoted to global
GlobalAddr, // user wants this to be a global symbol
GlobalAddrExport, // global symbol that is exported to linkers
ExternalAddr, // reference to address outside program (e.g. platform sym file)
Constant // constant value
}
/// Returns true if the symbol's type is an internal label (auto or user). Returns
/// false for external addresses and constants.
/// </summary>
public bool IsInternalLabel {
get {
// Could also check Type instead. Either works for now.
return SymbolSource == Source.User || SymbolSource == Source.Auto;
}
}
/// <summary>
/// Label sent to assembler.
/// </summary>
public string Label { get; private set; }
/// <summary>
/// Symbol's numeric value.
/// </summary>
public int Value { get; private set; }
/// <summary>
/// Symbol origin, e.g. auto-generated or entered by user.
/// </summary>
public Source SymbolSource { get; private set; }
/// <summary>
/// Type of symbol, e.g. local or global.
/// </summary>
public Type SymbolType { get; private set; }
/// <summary>
/// Two-character string representation of Source and Type, for display in the UI.
/// </summary>
public string SourceTypeString { get; private set; }
// No nullary constructor.
private Symbol() { }
/// <summary>
/// Constructs immutable object.
/// </summary>
/// <param name="label">Label string. Syntax assumed valid.</param>
/// <param name="source">User-defined or auto-generated?</param>
/// <param name="type">Type of symbol this is.</param>
/// user-defined.</param>
public Symbol(string label, int value, Source source, Type type) {
Debug.Assert(!string.IsNullOrEmpty(label));
Label = label;
Value = value;
SymbolType = type;
SymbolSource = source;
// Generate SourceTypeString.
string sts;
switch (SymbolSource) {
case Source.Auto: sts = "A"; break;
case Source.User: sts = "U"; break;
case Source.Platform: sts = "P"; break;
case Source.Project: sts = "R"; break;
default: sts = "?"; break;
}
switch (SymbolType) {
case Type.LocalOrGlobalAddr: sts += "L"; break;
case Type.GlobalAddr: sts += "G"; break;
case Type.GlobalAddrExport: sts += "X"; break;
case Type.ExternalAddr: sts += "E"; break;
case Type.Constant: sts += "C"; break;
default: sts += "?"; break;
}
SourceTypeString = sts;
}
public override string ToString() {
return Label + "{" + SymbolSource + "," + SymbolType +
",val=$" + Value.ToString("x4") + "}";
}
public static bool operator ==(Symbol a, Symbol b) {
if (ReferenceEquals(a, b)) {
return true; // same object, or both null
}
if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) {
return false; // one is null
}
// All fields must be equal. Ignore SourceTypeString, since it's generated
// from Source and Type.
return Asm65.Label.LABEL_COMPARER.Equals(a.Label, b.Label) && a.Value == b.Value &&
a.SymbolSource == b.SymbolSource && a.SymbolType == b.SymbolType;
}
public static bool operator !=(Symbol a, Symbol b) {
return !(a == b);
}
public override bool Equals(object obj) {
return obj is Symbol && this == (Symbol)obj;
}
public override int GetHashCode() {
// Convert the label to upper case before computing the hash code, so that
// symbols with "foo" and "FOO" (which are equal) have the same hash code.
return Asm65.Label.ToNormal(Label).GetHashCode() ^
Value ^ (int)SymbolType ^ (int)SymbolSource;
}
}
}

215
SourceGenWPF/SymbolTable.cs Normal file
View File

@ -0,0 +1,215 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
namespace SourceGenWPF {
/// <summary>
/// List of all symbols, arranged primarily by label, but also accessible by value. All
/// symbols have a unique label.
/// </summary>
public class SymbolTable : IEnumerable<Symbol> {
/// <summary>
/// Primary storage. Provides fast lookup by label. The StringComparer we choose
/// determines how case sensitivity and Culture is handled.
private SortedList<string, Symbol> mSymbols =
new SortedList<string, Symbol>(Asm65.Label.LABEL_COMPARER);
/// <summary>
/// Same content, but ordered by value. Note the key and the value are the same object.
/// </summary>
private SortedList<Symbol, Symbol> mSymbolsByValue =
new SortedList<Symbol, Symbol>(new CompareByValue());
/// <summary>
/// Compare two symbols, primarily by value, secondarily by source, and tertiarily
/// by label. The primary SortedList guarantees that the label is unique, so we
/// should never have two equal Symbols in the list.
///
/// The type comparison ensures that project symbols appear before platform symbols,
/// so that you can "overwrite" a platform symbol with the same value.
/// </summary>
private class CompareByValue : IComparer<Symbol> {
public int Compare(Symbol a, Symbol b) {
if (a.Value < b.Value) {
return -1;
} else if (a.Value > b.Value) {
return 1;
}
if ((int)a.SymbolSource < (int)b.SymbolSource) {
return -1;
} else if ((int)a.SymbolSource > (int)b.SymbolSource) {
return 1;
}
// Equal values, check string. We'll get a match on Remove or when
// replacing an entry with itself, but no two Symbols in the list
// should have the same label.
return Asm65.Label.LABEL_COMPARER.Compare(a.Label, b.Label);
}
}
/// <summary>
/// This is incremented whenever the contents of the symbol table change. External
/// code can compare this against a previous value to see if anything has changed
/// since the last visit.
///
/// We could theoretically miss something at the 2^32 rollover. Not worried.
/// </summary>
public int ChangeSerial { get; private set; }
public SymbolTable() { }
// IEnumerable
public IEnumerator<Symbol> GetEnumerator() {
// .Values is documented as O(1)
return mSymbols.Values.GetEnumerator();
}
// IEnumerable
IEnumerator IEnumerable.GetEnumerator() {
return mSymbols.Values.GetEnumerator();
}
/// <summary>
/// Clears the symbol table.
/// </summary>
public void Clear() {
mSymbols.Clear();
mSymbolsByValue.Clear();
ChangeSerial++;
}
/// <summary>
/// Returns the number of symbols in the table.
/// </summary>
public int Count() {
Debug.Assert(mSymbolsByValue.Count == mSymbols.Count);
return mSymbols.Count;
}
/// <summary>
/// Adds the specified symbol to the list. Throws an exception if the symbol is
/// already present.
/// </summary>
public void Add(Symbol sym) {
// If Symbol with matching label is in list, this will throw an exception,
// and the by-value add won't happen.
mSymbols.Add(sym.Label, sym);
mSymbolsByValue.Add(sym, sym);
ChangeSerial++;
}
/// <summary>
/// Finds the specified symbol by label. Throws an exception if it's not found.
///
/// Adds the specified symbol to the list, or replaces it if it's already present.
/// </summary>
public Symbol this[string key] {
get {
Debug.Assert(mSymbolsByValue.Count == mSymbols.Count);
return mSymbols[key];
}
set {
// Replacing {"foo", 1} with ("foo", 2} works correctly for mSymbols, because
// the label is the unique key. For mSymbolsByValue we have to explicitly
// remove it, because the entire Symbol is used as the key.
mSymbols.TryGetValue(key, out Symbol oldValue);
if (oldValue != null) {
mSymbolsByValue.Remove(oldValue);
}
mSymbols[key] = value;
mSymbolsByValue[value] = value;
ChangeSerial++;
}
}
/// <summary>
/// Searches the table for symbols with matching address values. Ignores constants.
/// </summary>
/// <param name="value">Value to find.</param>
/// <returns>First matching symbol found, or null if nothing matched.</returns>
public Symbol FindAddressByValue(int value) {
// Get sorted list of values. This is documented as efficient.
IList<Symbol> values = mSymbolsByValue.Values;
//for (int i = 0; i < values.Count; i++) {
// if (values[i].Value == value && values[i].SymbolType != Symbol.Type.Constant) {
// return values[i];
// }
//}
int low = 0;
int high = values.Count - 1;
while (low <= high) {
int mid = (low + high) / 2;
Symbol midValue = values[mid];
if (midValue.Value == value) {
// found a match, walk back to find first match
while (mid > 0 && values[mid - 1].Value == value) {
mid--;
}
// now skip past constants
while (mid < values.Count && values[mid].SymbolType == Symbol.Type.Constant) {
//Debug.WriteLine("disregarding " + values[mid]);
mid++;
}
if (mid < values.Count && values[mid].Value == value) {
return values[mid];
}
//Debug.WriteLine("Found value " + value + " but only constants");
return null;
} else if (midValue.Value < value) {
// move the low end in
low = mid + 1;
} else {
// move the high end in
Debug.Assert(midValue.Value > value);
high = mid - 1;
}
}
// not found
return null;
}
/// <summary>
/// Gets the value associated with the key.
/// </summary>
/// <param name="key">Label to look up.</param>
/// <param name="sym">Symbol, or null if not found.</param>
/// <returns>True if the key is present, false otherwise.</returns>
public bool TryGetValue(string key, out Symbol sym) {
return mSymbols.TryGetValue(key, out sym);
}
/// <summary>
/// Removes the specified symbol.
/// </summary>
public void Remove(Symbol sym) {
mSymbols.Remove(sym.Label);
mSymbolsByValue.Remove(sym);
ChangeSerial++;
}
}
}

View File

@ -0,0 +1,140 @@
/*
* 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 Asm65;
namespace SourceGenWPF {
/// <summary>
/// Helper functions for extracting values from a SystemDef instance.
/// </summary>
public static class SystemDefaults {
private const string LOAD_ADDRESS = "load-address";
private const string ENTRY_FLAGS = "entry-flags";
private const string UNDOCUMENTED_OPCODES = "undocumented-opcodes";
private const string FIRST_WORD_IS_LOAD_ADDR = "first-word-is-load-addr";
private const string ENTRY_FLAG_EMULATION = "emulation";
private const string ENTRY_FLAG_NATIVE_LONG = "native-long";
private const string ENTRY_FLAG_NATIVE_SHORT = "native-short";
/// <summary>
/// Gets the default load address.
/// </summary>
/// <param name="sysDef">SystemDef instance.</param>
/// <returns>Specified load address, or 0x1000 if nothing defined.</returns>
public static int GetLoadAddress(SystemDef sysDef) {
Dictionary<string, string> parms = sysDef.Parameters;
int retVal = 0x1000;
if (parms.TryGetValue(LOAD_ADDRESS, out string valueStr)) {
valueStr = valueStr.Trim();
if (Number.TryParseInt(valueStr, out int parseVal, out int unused)) {
retVal = parseVal;
} else {
Debug.WriteLine("WARNING: bad value for " + LOAD_ADDRESS + ": " + valueStr);
}
}
return retVal;
}
/// <summary>
/// Gets the default entry processor status flags.
/// </summary>
/// <param name="sysDef">SystemDef instance.</param>
/// <returns>Status flags.</returns>
public static StatusFlags GetEntryFlags(SystemDef sysDef) {
Dictionary<string, string> parms = sysDef.Parameters;
StatusFlags retFlags = StatusFlags.AllIndeterminate;
// On 65802/65816, this selects emulation mode. On 8-bit CPUs, these have
// no effect, but this reflects how the CPU behaves (short regs, emu mode).
retFlags.E = retFlags.M = retFlags.X = 1;
// Decimal mode is rarely used, and interrupts are generally enabled. Projects
// that need to assume otherwise can alter the entry flags. I want to start
// with decimal mode clear because it affects the cycle timing display on a
// number of 65C02 instructions.
retFlags.D = retFlags.I = 0;
if (parms.TryGetValue(ENTRY_FLAGS, out string valueStr)) {
switch (valueStr) {
case ENTRY_FLAG_EMULATION:
break;
case ENTRY_FLAG_NATIVE_LONG:
retFlags.E = retFlags.M = retFlags.X = 0;
break;
case ENTRY_FLAG_NATIVE_SHORT:
retFlags.E = 0;
break;
default:
Debug.WriteLine("WARNING: bad value for " + ENTRY_FLAGS +
": " + valueStr);
break;
}
}
return retFlags;
}
/// <summary>
/// Gets the default setting for undocumented opcode support.
/// </summary>
/// <param name="sysDef">SystemDef instance.</param>
/// <returns>Enable/disable value.</returns>
public static bool GetUndocumentedOpcodes(SystemDef sysDef) {
return GetBoolParam(sysDef, UNDOCUMENTED_OPCODES, false);
}
/// <summary>
/// Gets the default setting for using the first two bytes of the file as the
/// load address.
///
/// This is primarily for C64. Apple II DOS 3.3 binary files also put the load
/// address first, followed by the length, but that's typically stripped out when
/// the file is extracted.
/// </summary>
/// <param name="sysDef"></param>
/// <returns></returns>
public static bool GetFirstWordIsLoadAddr(SystemDef sysDef) {
return GetBoolParam(sysDef, FIRST_WORD_IS_LOAD_ADDR, false);
}
/// <summary>
/// Looks for a parameter with a matching name and a boolean value.
/// </summary>
/// <param name="sysDef">SystemDef reference.</param>
/// <param name="paramName">Name of parameter to look for.</param>
/// <param name="defVal">Default value.</param>
/// <returns>Parsed value, or defVal if the parameter doesn't exist or the value is not
/// a boolean string.</returns>
private static bool GetBoolParam(SystemDef sysDef, string paramName, bool defVal) {
Dictionary<string, string> parms = sysDef.Parameters;
bool retVal = defVal;
if (parms.TryGetValue(paramName, out string valueStr)) {
if (bool.TryParse(valueStr, out bool parseVal)) {
retVal = parseVal;
} else {
Debug.WriteLine("WARNING: bad value for " + paramName + ": " + valueStr);
}
}
return retVal;
}
}
}

213
SourceGenWPF/SystemDefs.cs Normal file
View File

@ -0,0 +1,213 @@
/*
* 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.IO;
using System.Linq;
using System.Text;
using System.Web.Script.Serialization;
using Asm65;
namespace SourceGenWPF {
/// <summary>
/// Target system definition, read from a config file.
/// </summary>
public class SystemDef {
// Fields are deserialized from JSON. Do not change the field names without updating
// the config files.
public string Name { get; set; }
public string GroupName { get; set; }
public string Cpu { get; set; }
public float Speed { get; set; }
public string Description { get; set; }
public string[] SymbolFiles { get; set; }
public string[] ExtensionScripts { get; set; }
public Dictionary<string, string> Parameters { get; set; }
/// <summary>
/// Generates a human-readable summary of this system definition for display to
/// the user.
/// </summary>
/// <returns>Multi-line string</returns>
public string GetSummaryString() {
StringBuilder sb = new StringBuilder();
sb.Append(Description);
sb.Append("\r\n\r\n");
sb.AppendFormat(Res.Strings.SETUP_SYSTEM_SUMMARY_FMT, Name, Cpu, Speed);
if (SymbolFiles.Length > 0) {
sb.Append("\r\n\r\n");
sb.Append(Res.Strings.INITIAL_SYMBOL_FILES);
foreach (string str in SymbolFiles) {
sb.Append("\r\n ");
ExternalFile ef = ExternalFile.CreateFromIdent(str);
if (ef == null) {
// Shouldn't happen unless somebody botches an edit.
sb.Append("[INVALID] " + str);
} else {
sb.Append(ef.GetInnards());
}
}
}
if (ExtensionScripts.Length > 0) {
sb.Append("\r\n\r\n");
sb.Append(Res.Strings.INITIAL_EXTENSION_SCRIPTS);
foreach (string str in ExtensionScripts) {
sb.Append("\r\n ");
ExternalFile ef = ExternalFile.CreateFromIdent(str);
if (ef == null) {
// Shouldn't happen unless somebody botches an edit.
sb.Append("[INVALID] " + str);
} else {
sb.Append(ef.GetInnards());
}
}
}
if (Parameters.Count > 0) {
sb.Append("\r\n\r\n");
sb.Append(Res.Strings.INITIAL_PARAMETERS);
foreach (KeyValuePair<string, string> kvp in Parameters) {
sb.Append("\r\n ");
sb.Append(kvp.Key);
sb.Append(" = ");
sb.Append(kvp.Value);
}
}
return sb.ToString();
}
/// <summary>
/// Validates the values read from JSON.
/// </summary>
/// <returns>True if the inputs are valid and complete.</returns>
public bool Validate() {
if (string.IsNullOrEmpty(Name)) {
return false;
}
if (string.IsNullOrEmpty(GroupName)) {
return false;
}
if (CpuDef.GetCpuTypeFromName(Cpu) == CpuDef.CpuType.CpuUnknown) {
return false;
}
if (Speed == 0.0f) {
return false;
}
if (SymbolFiles == null || ExtensionScripts == null || Parameters == null) {
// We don't really need to require these, but it's probably best to
// insist on fully-formed entries.
return false;
}
// Disallow file idents that point outside the runtime directory. I don't think
// there's any harm in allowing it, but there's currently no value in it either.
foreach (string str in SymbolFiles) {
if (!str.StartsWith("RT:")) {
return false;
}
}
foreach (string str in ExtensionScripts) {
if (!str.StartsWith("RT:")) {
return false;
}
}
return true;
}
public override string ToString() {
StringBuilder symFilesStr = new StringBuilder();
foreach (string str in SymbolFiles) {
if (symFilesStr.Length != 0) {
symFilesStr.Append(", ");
}
symFilesStr.Append(str);
}
StringBuilder scriptFilesStr = new StringBuilder();
foreach (string str in ExtensionScripts) {
if (scriptFilesStr.Length != 0) {
scriptFilesStr.Append(", ");
}
scriptFilesStr.Append(str);
}
StringBuilder paramStr = new StringBuilder();
foreach (KeyValuePair<string, string> kvp in Parameters) {
if (paramStr.Length != 0) {
paramStr.Append(", ");
}
paramStr.Append(kvp.Key);
paramStr.Append('=');
paramStr.Append(kvp.Value);
}
return "'" + Name + "', '" + GroupName + "', " + Cpu + " @ " + Speed + "MHz" +
", sym={" + symFilesStr + "}, scr={" + scriptFilesStr + "}, par={" +
paramStr + "}";
}
}
/// <summary>
/// System definition collection.
/// </summary>
public class SystemDefSet {
// Identification string, embedded in the JSON data.
const string MAGIC = "6502bench SourceGen sysdef v1";
// Fields are deserialized from JSON. Do not change the field names without updating
// the config files.
public string Contents { get; set; }
public SystemDef[] Defs { get; set; }
/// <summary>
/// Empty constructor, required for deserialization.
/// </summary>
public SystemDefSet() {}
/// <summary>
/// Reads the named config file. Throws an exception on failure.
/// </summary>
/// <param name="pathName">Config file path name</param>
/// <returns>Fully-populated system defs.</returns>
public static SystemDefSet ReadFile(string pathName) {
string fileStr = File.ReadAllText(pathName);
//Debug.WriteLine("READ " + fileStr);
JavaScriptSerializer ser = new JavaScriptSerializer();
SystemDefSet sdf = ser.Deserialize<SystemDefSet>(fileStr);
if (sdf.Contents != MAGIC) {
// This shouldn't happen unless somebody is tampering with the
// config file.
Debug.WriteLine("Expected contents '" + MAGIC + "', got " +
sdf.Contents + "'");
throw new InvalidDataException("Sys def file '" + pathName +
"': Unexpected contents '" + sdf.Contents + "'");
}
foreach (SystemDef sd in sdf.Defs) {
Debug.WriteLine("### " + sd);
}
return sdf;
}
}
}

View File

@ -0,0 +1,460 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Diagnostics;
using Asm65;
using CommonUtil;
/*
*** When is a full (code+data) re-analysis required?
- Adding/removing/changing an address change (ORG directive). This has a significant impact
on the code analyzer, as blocks of code may become reachable or unreachable.
- Adding/removing/changing a type hint. These can affect whether a given offset is treated
as code, which can have a dramatic effect on code analysis (consider the offset 0 code hint).
- Adding/removing/changing a status flag override. This can affect whether a branch is
always taken or never taken, and the M/X flags affect instruction interpretation. (It may
be possible to do an "incremental" code analysis here, working from the point of the change
forward, propagating changes outward, but that gets tricky when a branch changes from
ambiguously-taken to never-taken, and the destination may need to be treated as data.)
*** When is a partial (data-only) re-analysis required?
- Adding/removing a user label. The code that tries to adjust data targets to match nearby
user labels must be re-run, possibly impacting auto-generated labels. A user label added
to the middle of a multi-byte data element will cause the element to be split, requiring
reanalysis of the pieces.
- Adding/removing/changing an operand label, e.g "LDA label". This can affect which
offsets are marked as data targets, which affects the data analyzer. (We could be smart
about this and not invoke reanalysis if the label value matches the operand, but address
operands should already have labels via offset reference, so it's unclear how valuable
this would be.)
- Adding/removing/changing a format descriptor with a symbol or Numeric/Address. This
can affect the data target analysis.
*** When is a partial (late-data) re-analysis required?
- Adding/removing/changing the length of a formatted data item, when that item isn't subject
to conditions above (e.g. the descriptor doesn't specify a symbol). This affects which bytes
are considered "uncategorized", so the uncategorized-data analysis must be repeated.
*** When is display-only re-analysis needed?
- When altering the way that data is formatted, it's useful to exercise the same code paths,
up to the point where the analyzer is called. We still want to go through all the steps that
update the display list and cause controls to be redrawn, but we don't want to actually change
anything in the DisasmProject. "Misc" means we do nothing but pretend there was a full update.
*** When can we get away with only updating part of the display list (re-analysis=none)?
- Changing a user label. All lines that reference the label need to be updated in the
display, but nothing in the analysis changes. (This assumes we prevent you from renaming
a label to be the same as an existing label, e.g. auto-generated labels.)
- Adding/removing/changing cosmetic items, like comments and notes.
NOTE: all re-analysis requirements are symmetric for undo/redo. Undoing a change requires
the same level of work as doing the change.
*/
namespace SourceGenWPF {
/// <summary>
/// A single change.
/// </summary>
public class UndoableChange {
public enum ChangeType {
Unknown = 0,
// Dummy change, used to force a full update.
Dummy,
// Adds, updates, or removes an AddressMap entry.
SetAddress,
// Changes the type hint.
SetTypeHint,
// Adds, updates, or removes a processor status flag override.
SetStatusFlagOverride,
// Adds, updates, or removes a user-specified label.
SetLabel,
// Adds, updates, or removes a data or operand format.
SetOperandFormat,
// Changes the end-of-line comment.
SetComment,
// Changes the long comment.
SetLongComment,
// Changes the note.
SetNote,
// Updates project properties.
SetProjectProperties
}
/// <summary>
/// Enum indicating what needs to be reanalyzed after a change.
/// </summary>
public enum ReanalysisScope {
None = 0,
DisplayOnly,
DataOnly,
CodeAndData
}
/// <summary>
/// Identifies the change type.
/// </summary>
public ChangeType Type { get; private set; }
/// <summary>
/// The "root offset". For example, changing the type hint for a 4-byte
/// instruction from code to data will actually affect 4 offsets, but we
/// only need to specify the root item.
/// </summary>
public int Offset { get; private set; }
/// <summary>
/// Value we're changing to.
/// </summary>
public object NewValue { get; private set; }
/// <summary>
/// Previous value, used for "undo".
/// </summary>
public object OldValue { get; private set; }
/// <summary>
/// Indicates what amount of reanalysis is required after the change is implemented.
/// </summary>
public ReanalysisScope ReanalysisRequired { get; private set; }
// Don't instantiate directly.
private UndoableChange() { }
public bool HasOffset {
get {
switch (Type) {
case ChangeType.Dummy:
case ChangeType.SetTypeHint:
case ChangeType.SetProjectProperties:
return false;
default:
return true;
}
}
}
/// <summary>
/// Creates an UndoableChange that does nothing but force an update.
/// </summary>
/// <param name="flags">Desired reanalysis flags.</param>
/// <returns>Change record.</returns>
public static UndoableChange CreateDummyChange(ReanalysisScope flags) {
UndoableChange uc = new UndoableChange();
uc.Type = ChangeType.Dummy;
uc.Offset = -1;
uc.ReanalysisRequired = flags;
return uc;
}
/// <summary>
/// Creates an UndoableChange for an address map update.
/// </summary>
/// <param name="offset">Affected offset.</param>
/// <param name="oldAddress">Previous address map entry, or -1 if none.</param>
/// <param name="newAddress">New address map entry, or -1 if none.</param>
/// <returns>Change record.</returns>
public static UndoableChange CreateAddressChange(int offset, int oldAddress,
int newAddress) {
if (oldAddress == newAddress) {
Debug.WriteLine("No-op address change at +" + offset.ToString("x6") +
": " + oldAddress);
}
UndoableChange uc = new UndoableChange();
uc.Type = ChangeType.SetAddress;
uc.Offset = offset;
uc.OldValue = oldAddress;
uc.NewValue = newAddress;
uc.ReanalysisRequired = ReanalysisScope.CodeAndData;
return uc;
}
/// <summary>
/// Creates an UndoableChange for a type hint update. Rather than adding a
/// separate UndoableChange for each affected offset -- which could span the
/// entire file -- we use range sets to record the before/after state.
/// </summary>
/// <param name="undoSet">Current values.</param>
/// <param name="newSet">New values.</param>
/// <returns>Change record.</returns>
public static UndoableChange CreateTypeHintChange(TypedRangeSet undoSet,
TypedRangeSet newSet) {
if (newSet.Count == 0) {
Debug.WriteLine("Empty hint change?");
}
UndoableChange uc = new UndoableChange();
uc.Type = ChangeType.SetTypeHint;
uc.Offset = -1;
uc.OldValue = undoSet;
uc.NewValue = newSet;
// Any hint change can affect whether something is treated as code.
// Either we're deliberately setting it as code or non-code, or we're
// setting it to "no hint", which means the code analyzer gets
// to make the decision now. This requires a full code+data re-analysis.
uc.ReanalysisRequired = ReanalysisScope.CodeAndData;
return uc;
}
/// <summary>
/// Creates an UndoableChange for a status flag override update.
/// </summary>
/// <param name="offset">Affected offset.</param>
/// <param name="oldFlags">Current flags.</param>
/// <param name="newFlags">New flags.</param>
/// <returns></returns>
public static UndoableChange CreateStatusFlagChange(int offset, StatusFlags oldFlags,
StatusFlags newFlags) {
if (oldFlags == newFlags) {
Debug.WriteLine("No-op status flag change at " + offset);
}
UndoableChange uc = new UndoableChange();
uc.Type = ChangeType.SetStatusFlagOverride;
uc.Offset = offset;
uc.OldValue = oldFlags;
uc.NewValue = newFlags;
// This can affect instruction widths (for M/X) and conditional branches. We
// don't need to re-analyze for changes to I/D, but users don't really need to
// change those anyway, so it's not worth optimizing.
uc.ReanalysisRequired = ReanalysisScope.CodeAndData;
return uc;
}
/// <summary>
/// Creates an UndoableChange for a label update.
/// </summary>
/// <param name="offset">Affected offset.</param>
/// <param name="oldSymbol">Current label. May be null.</param>
/// <param name="newSymbol">New label. May be null.</param>
/// <returns>Change record.</returns>
public static UndoableChange CreateLabelChange(int offset, Symbol oldSymbol,
Symbol newSymbol) {
if (oldSymbol == newSymbol) {
Debug.WriteLine("No-op label change at +" + offset.ToString("x6") +
": " + oldSymbol);
}
UndoableChange uc = new UndoableChange();
uc.Type = ChangeType.SetLabel;
uc.Offset = offset;
uc.OldValue = oldSymbol;
uc.NewValue = newSymbol;
// Data analysis can change if we add or remove a label in a data area. Label
// selection can change as well, e.g. switching from an auto-label to a user
// label with an adjustment. So renaming a user-defined label doesn't require
// reanalysis, but adding or removing one does.
//
// Do the reanalysis if either is empty. This will cause an unnecessary
// reanalysis if we change an empty label to an empty label, but that shouldn't
// be allowed by the UI anyway.
Debug.Assert(newSymbol == null || newSymbol.SymbolSource == Symbol.Source.User);
if ((oldSymbol == null) || (newSymbol == null) /*||
(oldSymbol.SymbolSource != newSymbol.SymbolSource)*/) {
uc.ReanalysisRequired = ReanalysisScope.DataOnly;
} else {
uc.ReanalysisRequired = ReanalysisScope.None;
}
return uc;
}
/// <summary>
/// Creates an UndoableChange for an operand or data format update. This method
/// refuses to create a change for a no-op, returning null instead. This will
/// convert a FormatDescriptor with type REMOVE to null, with the intention of
/// removing the descriptor from the format set.
/// </summary>
/// <param name="offset">Affected offset.</param>
/// <param name="oldFormat">Current format. May be null.</param>
/// <param name="newFormat">New format. May be null.</param>
/// <returns>Change record, or null for a no-op change.</returns>
public static UndoableChange CreateActualOperandFormatChange(int offset,
FormatDescriptor oldFormat, FormatDescriptor newFormat) {
if (newFormat != null && newFormat.FormatType == FormatDescriptor.Type.REMOVE) {
Debug.WriteLine("CreateOperandFormatChange: converting REMOVE to null");
newFormat = null;
}
if (oldFormat == newFormat) {
Debug.WriteLine("No-op format change at +" + offset.ToString("x6") +
": " + oldFormat);
return null;
}
return CreateOperandFormatChange(offset, oldFormat, newFormat);
}
/// <summary>
/// Creates an UndoableChange for an operand or data format update.
/// </summary>
/// <param name="offset">Affected offset.</param>
/// <param name="oldFormat">Current format. May be null.</param>
/// <param name="newFormat">New format. May be null.</param>
/// <returns>Change record.</returns>
public static UndoableChange CreateOperandFormatChange(int offset,
FormatDescriptor oldFormat, FormatDescriptor newFormat) {
if (oldFormat == newFormat) {
Debug.WriteLine("No-op format change at +" + offset.ToString("x6") +
": " + oldFormat);
}
// We currently allow old/new formats with different lengths. There doesn't
// seem to be a reason not to, and a slight performance advantage to doing so.
// Also, if a change set has two changes at the same offset, undo requires
// enumerating the list in reverse order.
UndoableChange uc = new UndoableChange();
uc.Type = ChangeType.SetOperandFormat;
uc.Offset = offset;
uc.OldValue = oldFormat;
uc.NewValue = newFormat;
// Data-only reanalysis is required if the old or new format has a label. Simply
// changing from e.g. default to decimal, or decimal to binary, doesn't matter.
// (The format editing code ensures that labels don't appear in the middle of
// a formatted region.) Adding, removing, or changing a symbol can change the
// layout of uncategorized data, affect data targets, xrefs, etc.
//
// We can't only check for a symbol, though, because Numeric/Address will
// create an auto-label if the reference is within the file.
//
// If the number of bytes covered by the format changes, or we're adding or
// removing a format, we need to redo the analysis of uncategorized data. For
// example, an auto-detected string could get larger or smaller. We don't
// currently have a separate flag for just that. Also, because we're focused
// on just one change, we can't skip reanalysis when (say) one 4-byte numeric
// is converted to two two-byte numerics.
if ((oldFormat != null && oldFormat.HasSymbolOrAddress) ||
(newFormat != null && newFormat.HasSymbolOrAddress)) {
uc.ReanalysisRequired = ReanalysisScope.DataOnly;
} else if (oldFormat == null || newFormat == null ||
oldFormat.Length != newFormat.Length) {
uc.ReanalysisRequired = ReanalysisScope.DataOnly;
} else {
uc.ReanalysisRequired = ReanalysisScope.None;
}
return uc;
}
/// <summary>
/// Creates an UndoableChange for a comment update.
/// </summary>
/// <param name="offset">Affected offset.</param>
/// <param name="oldComment">Current comment.</param>
/// <param name="newComment">New comment.</param>
/// <returns>Change record.</returns>
public static UndoableChange CreateCommentChange(int offset, string oldComment,
string newComment) {
if (oldComment.Equals(newComment)) {
Debug.WriteLine("No-op comment change at +" + offset.ToString("x6") +
": " + oldComment);
}
UndoableChange uc = new UndoableChange();
uc.Type = ChangeType.SetComment;
uc.Offset = offset;
uc.OldValue = oldComment;
uc.NewValue = newComment;
uc.ReanalysisRequired = ReanalysisScope.None;
return uc;
}
/// <summary>
/// Creates an UndoableChange for a long comment update.
/// </summary>
/// <param name="offset">Affected offset.</param>
/// <param name="oldComment">Current comment.</param>
/// <param name="newComment">New comment.</param>
/// <returns>Change record.</returns>
public static UndoableChange CreateLongCommentChange(int offset,
MultiLineComment oldComment, MultiLineComment newComment) {
if (oldComment == newComment) {
Debug.WriteLine("No-op long comment change at +" + offset.ToString("x6") +
": " + oldComment);
}
UndoableChange uc = new UndoableChange();
uc.Type = ChangeType.SetLongComment;
uc.Offset = offset;
uc.OldValue = oldComment;
uc.NewValue = newComment;
uc.ReanalysisRequired = ReanalysisScope.None;
return uc;
}
/// <summary>
/// Creates an UndoableChange for a note update.
/// </summary>
/// <param name="offset">Affected offset.</param>
/// <param name="oldNote">Current note.</param>
/// <param name="newNote">New note.</param>
/// <returns>Change record.</returns>
public static UndoableChange CreateNoteChange(int offset,
MultiLineComment oldNote, MultiLineComment newNote) {
if (oldNote == newNote) {
Debug.WriteLine("No-op note change at +" + offset.ToString("x6") +
": " + oldNote);
}
UndoableChange uc = new UndoableChange();
uc.Type = ChangeType.SetNote;
uc.Offset = offset;
uc.OldValue = oldNote;
uc.NewValue = newNote;
uc.ReanalysisRequired = ReanalysisScope.None;
return uc;
}
/// <summary>
/// Creates an UndoableChange for a change to the project properties.
/// </summary>
/// <param name="oldNote">Current note.</param>
/// <param name="newNote">New note.</param>
/// <returns>Change record.</returns>
public static UndoableChange CreateProjectPropertiesChange(ProjectProperties oldProps,
ProjectProperties newProps) {
Debug.Assert(oldProps != null && newProps != null);
if (oldProps == newProps) { // doesn't currently work except as reference check
Debug.WriteLine("No-op property change: " + oldProps);
}
UndoableChange uc = new UndoableChange();
uc.Type = ChangeType.SetProjectProperties;
uc.Offset = -1;
uc.OldValue = oldProps;
uc.NewValue = newProps;
// Project properties could change the CPU type, requiring a full code+data
// reanalysis. We could scan the objects to see what actually changed, but that
// doesn't seem worthwhile.
uc.ReanalysisRequired = ReanalysisScope.CodeAndData;
return uc;
}
public override string ToString() {
return "[UC type=" + Type + " offset=+" +
(HasOffset ? Offset.ToString("x6") : "N/A") + "]";
}
}
}

View File

@ -0,0 +1,146 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using CommonUtil;
namespace SourceGenWPF {
/// <summary>
/// Tracks the items selected in a list view.
///
/// Forward the ItemSelectionChanged and VirtualItemsSelectionRangeChanged.
/// </summary>
public class VirtualListViewSelection {
private BitArray mSelection;
/// <summary>
/// Retrieves the total number of boolean values in the set. This is NOT the
/// number of selected items.
/// </summary>
public int Length { get { return mSelection.Length; } }
/// <summary>
/// Sets or gets the Nth element. True means the line is selected.
/// </summary>
public bool this[int key] {
get {
return mSelection[key];
}
set {
mSelection[key] = value;
}
}
public VirtualListViewSelection() {
mSelection = new BitArray(0);
}
public VirtualListViewSelection(int length) {
mSelection = new BitArray(length);
}
/// <summary>
/// Sets the length of the selection array.
///
/// If the new length is longer, the new elements are initialized to false. If the
/// new length is shorter, the excess elements are discarded. (This matches the behavior
/// of a virtual ListView selection set.)
/// </summary>
/// <param name="length">New length.</param>
public void SetLength(int length) {
//Debug.WriteLine("VirtualListViewSelection length now " + length);
mSelection.Length = length;
}
#if false // TODO
/// <summary>
/// Handle a state change for a single item.
/// </summary>
public void ItemSelectionChanged(ListViewItemSelectionChangedEventArgs e) {
//Debug.WriteLine("ItemSelectionChanged: " + e.ItemIndex + " (" + e.IsSelected + ")");
if (e.ItemIndex >= mSelection.Length) {
Debug.WriteLine("GLITCH: selection index " + e.ItemIndex + " out of range");
Debug.Assert(false);
return;
}
mSelection.Set(e.ItemIndex, e.IsSelected);
}
/// <summary>
/// Handle a state change for a range of items.
/// </summary>
public void VirtualItemsSelectionRangeChanged(
ListViewVirtualItemsSelectionRangeChangedEventArgs e) {
//Debug.WriteLine("VirtualRangeChange: " + e.StartIndex + " - " + e.EndIndex +
// " (" + e.IsSelected + ")");
if (e.StartIndex == 0 && e.EndIndex == mSelection.Length - 1) {
// Set all elements. The list view control seems to like to set all elements
// to false whenever working with multi-select, so this should be fast.
//Debug.WriteLine("VirtualRangeChange: set all to " + e.IsSelected);
mSelection.SetAll(e.IsSelected);
} else {
if (e.EndIndex >= mSelection.Length) {
Debug.WriteLine("GLITCH: selection end index " + e.EndIndex + " out of range");
Debug.Assert(false);
return;
}
bool val = e.IsSelected;
for (int i = e.StartIndex; i <= e.EndIndex; i++) {
mSelection.Set(i, val);
}
}
}
#endif
/// <summary>
/// Confirms that the selection count matches the number of set bits. Pass
/// in {ListView}.SelectedIndices.Count.
/// </summary>
/// <param name="expected">Expected number of selected entries.</param>
/// <returns>True if count matches.</returns>
public bool DebugValidateSelectionCount(int expected) {
int actual = 0;
foreach (bool bit in mSelection) {
if (bit) {
actual++;
}
}
if (actual != expected) {
Debug.WriteLine("SelectionCount expected " + expected + ", actual " + actual);
}
return (actual == expected);
}
public void DebugDump() {
RangeSet rangeSet = new RangeSet();
for (int i = 0; i < mSelection.Length; i++) {
if (mSelection[i]) {
rangeSet.Add(i);
}
}
Debug.WriteLine("VirtualListViewSelection ranges:");
IEnumerator<RangeSet.Range> iter = rangeSet.RangeListIterator;
while (iter.MoveNext()) {
RangeSet.Range range = iter.Current;
Debug.WriteLine(" [" + range.Low.ToString() + "," + range.High.ToString() + "]");
}
}
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Diagnostics;
namespace SourceGenWPF {
/// <summary>
/// Weak reference to a symbol for use in an operand or data statement. The reference
/// is by name; if the symbol disappears or changes value, the reference can be ignored.
/// This also specifies which part of the numeric value is of interest, so we can reference
/// the high or low byte of a 16-bit value in (say) LDA #imm.
///
/// Instances are immutable.
/// </summary>
public class WeakSymbolRef {
/// <summary>
/// This identifies the part of the value that we're interested in. All values are
/// signed 32-bit integers.
/// </summary>
public enum Part {
// This indicates which byte we start with, useful for immediate operands
// and things like PEA. By popular convention, these are referred to as
// low, high, and bank.
//
// With 16-bit registers, Merlin 32 grabs the high *word*, while cc65's assembler
// grabs the high *byte*. One is a shift, the other is a byte select. We use
// low/high/bank just to mean position here.
//
// (Could make this orthogonal with a pair of bit fields, one for position and
// one for width, but there's really only three widths of interest (1, 2, 3 bytes)
// and that's defined by context.)
Unknown = 0,
Low, // LDA #label, LDA #<label
High, // LDA #>label
Bank, // LDA #^label
}
/// <summary>
/// Label of symbol of interest.
/// </summary>
public string Label { get; private set; }
/// <summary>
/// Which part of the value we're referencing.
/// </summary>
public Part ValuePart { get; private set; }
/// <summary>
/// Full constructor.
/// </summary>
public WeakSymbolRef(string label, Part part) {
Debug.Assert(label != null);
Label = label;
ValuePart = part;
}
public static bool operator ==(WeakSymbolRef a, WeakSymbolRef 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 Asm65.Label.LABEL_COMPARER.Equals(a.Label, b.Label) &&
a.ValuePart == b.ValuePart;
}
public static bool operator !=(WeakSymbolRef a, WeakSymbolRef b) {
return !(a == b);
}
public override bool Equals(object obj) {
return obj is WeakSymbolRef && this == (WeakSymbolRef)obj;
}
public override int GetHashCode() {
return Asm65.Label.ToNormal(Label).GetHashCode() ^ (int)ValuePart;
}
public override string ToString() {
return "WeakSym: " + Label + ":" + ValuePart;
}
}
}

139
SourceGenWPF/XrefSet.cs Normal file
View File

@ -0,0 +1,139 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
namespace SourceGenWPF {
/// <summary>
/// Tracks a set of offsets that reference a single address or label.
///
/// This is used internally, when refactoring labels, as well as for the "references"
/// UI panel and label localizer.
/// </summary>
public class XrefSet : IEnumerable<XrefSet.Xref> {
/// <summary>
/// Reference type. This is mostly useful for display to the user.
/// </summary>
public enum XrefType {
Unknown = 0,
SubCallOp, // subroutine call
BranchOp, // branch instruction
RefFromData, // reference in data area, e.g. ".dd2 <address>"
MemAccessOp, // instruction that accesses memory, or refers to an address
// TODO(someday): track 16-bit vs. 24-bit addressing, so we can show whether
// something is a "far" reference (and maybe carry this into auto-label annotation)
}
/// <summary>
/// Cross-reference descriptor. Instances are immutable.
/// </summary>
public class Xref {
/// <summary>
/// Offset of start of instruction or data that refers to the target offset.
/// </summary>
public int Offset { get; private set; }
/// <summary>
/// True if this reference is by name.
/// </summary>
public bool IsSymbolic { get; private set; }
/// <summary>
/// Type of reference.
/// </summary>
public XrefType Type { get; private set; }
/// <summary>
/// For Type==MemAccessOp, what type of memory access is performed.
/// </summary>
public Asm65.OpDef.MemoryEffect AccType { get; private set; }
/// <summary>
/// Adjustment to symbol. For example, "LDA label+2" adds an xref entry to
/// "label", with an adjustment of +2.
/// </summary>
public int Adjustment { get; private set; }
public Xref(int offset, bool isSymbolic, XrefType type,
Asm65.OpDef.MemoryEffect accType, int adjustment) {
Offset = offset;
IsSymbolic = isSymbolic;
Type = type;
AccType = accType;
Adjustment = adjustment;
}
public override string ToString() {
return "Xref off=+" + Offset.ToString("x6") + " sym=" + IsSymbolic +
" type=" + Type + " accType= " + AccType + " adj=" + Adjustment;
}
}
/// <summary>
/// Internal storage for xrefs.
/// </summary>
private List<Xref> mRefs = new List<Xref>();
/// <summary>
/// Constructs an empty set.
/// </summary>
public XrefSet() { }
/// <summary>
/// Returns the number of cross-references in the set.
/// </summary>
public int Count { get { return mRefs.Count; } }
/// <summary>
/// Removes all entries from the set.
/// </summary>
public void Clear() {
mRefs.Clear();
}
/// <summary>
/// Returns the Nth entry in the set.
/// </summary>
public Xref this[int index] {
get {
return mRefs[index];
}
}
/// <summary>
/// Adds an xref to the set.
/// </summary>
public void Add(Xref xref) {
// TODO(someday): not currently enforcing set behavior; start by adding .equals to
// Xref, then check Contains before allowing Add. (Should probably complain
// loudly if item already exists, since we're not expecting that.)
mRefs.Add(xref);
}
// IEnumerable
public IEnumerator GetEnumerator() {
return ((IEnumerable)mRefs).GetEnumerator();
}
// IEnumerable, generic
IEnumerator<Xref> IEnumerable<Xref>.GetEnumerator() {
return ((IEnumerable<Xref>)mRefs).GetEnumerator();
}
}
}