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:
parent
8ceae370cc
commit
575f834b1d
296
SourceGenWPF/AddressMap.cs
Normal file
296
SourceGenWPF/AddressMap.cs
Normal 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
355
SourceGenWPF/Anattrib.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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
355
SourceGenWPF/AppSettings.cs
Normal 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
164
SourceGenWPF/AutoLabel.cs
Normal 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
109
SourceGenWPF/ChangeSet.cs
Normal 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
1070
SourceGenWPF/CodeAnalysis.cs
Normal file
File diff suppressed because it is too large
Load Diff
1139
SourceGenWPF/DataAnalysis.cs
Normal file
1139
SourceGenWPF/DataAnalysis.cs
Normal file
File diff suppressed because it is too large
Load Diff
104
SourceGenWPF/DefSymbol.cs
Normal file
104
SourceGenWPF/DefSymbol.cs
Normal 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 + "]");
|
||||
}
|
||||
}
|
||||
}
|
1767
SourceGenWPF/DisasmProject.cs
Normal file
1767
SourceGenWPF/DisasmProject.cs
Normal file
File diff suppressed because it is too large
Load Diff
1224
SourceGenWPF/DisplayList.cs
Normal file
1224
SourceGenWPF/DisplayList.cs
Normal file
File diff suppressed because it is too large
Load Diff
264
SourceGenWPF/ExternalFile.cs
Normal file
264
SourceGenWPF/ExternalFile.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
423
SourceGenWPF/FormatDescriptor.cs
Normal file
423
SourceGenWPF/FormatDescriptor.cs
Normal 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 + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
SourceGenWPF/HelpAccess.cs
Normal file
90
SourceGenWPF/HelpAccess.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
276
SourceGenWPF/MultiLineComment.cs
Normal file
276
SourceGenWPF/MultiLineComment.cs
Normal 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
157
SourceGenWPF/NavStack.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
254
SourceGenWPF/PlatformSymbols.cs
Normal file
254
SourceGenWPF/PlatformSymbols.cs
Normal 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)");
|
||||
}
|
||||
}
|
||||
}
|
@ -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
743
SourceGenWPF/ProjectFile.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
134
SourceGenWPF/ProjectProperties.cs
Normal file
134
SourceGenWPF/ProjectProperties.cs
Normal 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
897
SourceGenWPF/PseudoOp.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
BIN
SourceGenWPF/Res/SourceGenIcon.ico
Normal file
BIN
SourceGenWPF/Res/SourceGenIcon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 139 KiB |
38
SourceGenWPF/Res/Strings.xaml
Normal file
38
SourceGenWPF/Res/Strings.xaml
Normal 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>
|
71
SourceGenWPF/Res/Strings.xaml.cs
Normal file
71
SourceGenWPF/Res/Strings.xaml.cs
Normal 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");
|
||||
}
|
||||
}
|
113
SourceGenWPF/RuntimeDataAccess.cs
Normal file
113
SourceGenWPF/RuntimeDataAccess.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
255
SourceGenWPF/Sandbox/DomainManager.cs
Normal file
255
SourceGenWPF/Sandbox/DomainManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
237
SourceGenWPF/Sandbox/PluginDllCache.cs
Normal file
237
SourceGenWPF/Sandbox/PluginDllCache.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
226
SourceGenWPF/Sandbox/ScriptManager.cs
Normal file
226
SourceGenWPF/Sandbox/ScriptManager.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
177
SourceGenWPF/Sandbox/Sponsor.cs
Normal file
177
SourceGenWPF/Sandbox/Sponsor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
156
SourceGenWPF/Symbol.cs
Normal 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
215
SourceGenWPF/SymbolTable.cs
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
140
SourceGenWPF/SystemDefaults.cs
Normal file
140
SourceGenWPF/SystemDefaults.cs
Normal 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
213
SourceGenWPF/SystemDefs.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
460
SourceGenWPF/UndoableChange.cs
Normal file
460
SourceGenWPF/UndoableChange.cs
Normal 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") + "]";
|
||||
}
|
||||
}
|
||||
}
|
146
SourceGenWPF/VirtualListViewSelection.cs
Normal file
146
SourceGenWPF/VirtualListViewSelection.cs
Normal 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() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
94
SourceGenWPF/WeakSymbolRef.cs
Normal file
94
SourceGenWPF/WeakSymbolRef.cs
Normal 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
139
SourceGenWPF/XrefSet.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user