/*
* 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 Asm65;
using CommonUtil;
using SourceGen.Sandbox;
namespace SourceGen {
///
/// All state for an open project.
///
/// This class does no file I/O or user interaction.
///
public class DisasmProject {
// Arbitrary 1MB limit. Could be increased to 16MB if performance is acceptable.
public const int MAX_DATA_FILE_SIZE = 1 << 20;
// File magic.
private const long MAGIC = 6982516645493599905;
#region Data that is saved to the project file
// All data held by structures in this section are persistent, and will be
// written to the project file. Anything not in this section may be discarded
// at any time. Smaller items are kept in arrays, with one entry per byte
// of file data.
///
/// Length of input data. (This is redundant with FileData.Length while in memory,
/// but we want this value to be serialized into the project file.)
///
public int FileDataLength { get; private set; }
///
/// CRC-32 on input data.
///
public uint FileDataCrc32 { get; private set; }
///
/// Map file offsets to addresses.
///
public AddressMap AddrMap { get; private set; }
///
/// Type hints. Default value is "no hint".
///
public CodeAnalysis.TypeHint[] TypeHints { get; private set; }
///
/// Status flag overrides. Default value is "all unspecified".
///
public StatusFlags[] StatusFlagOverrides { get; private set; }
///
/// End-of-line comments. Empty string means "no comment".
///
public string[] Comments { get; private set; }
///
/// Full line, possibly multi-line comments.
///
public Dictionary LongComments { get; private set; }
///
/// Notes, which are like comments but not included in the assembled output.
///
public SortedList Notes { get; private set; }
///
/// Labels, defined by the user; uses file offset as key. Ideally the label names
/// are unique, but there are ways around that.
///
public Dictionary UserLabels { get; private set; }
///
/// Local variable tables.
///
public SortedList LvTables { get; private set; }
///
/// Format descriptors for operands and data items; uses file offset as key.
///
public SortedList OperandFormats { get; private set; }
///
/// Project properties. Includes CPU type, platform symbol file names, project
/// symbols, etc.
///
public ProjectProperties ProjectProps { get; private set; }
#endregion // data to save & restore
///
/// The contents of the 65xx data file.
///
public byte[] FileData { get { return mFileData; } }
private byte[] mFileData;
///
/// CPU definition to use when analyzing input.
///
public CpuDef CpuDef { get; private set; }
///
/// If set, changes are ignored.
///
public bool IsReadOnly { get; set; }
///
/// If true, plugins will execute in the main application's AppDomain instead of
/// the sandbox. Must be set before calling Initialize().
///
public bool UseMainAppDomainForPlugins { get; set; }
///
/// Full pathname of project file. The directory name is needed when loading
/// platform symbols and extension scripts from the project directory, and the
/// filename is used to give project-local extension scripts unique DLL names.
///
/// For a new project that hasn't been saved yet, this will be empty.
///
public string ProjectPathName { get; set; }
// Filename only of data file. This is used for debugging and text export.
public string DataFileName { get; private set; }
// This holds working state for the code and data analyzers. Some of the state
// is presented directly to the user, e.g. status flags. All of the data here
// should be considered transient; it may be discarded at any time without
// causing user data loss.
private Anattrib[] mAnattribs;
// A snapshot of the Anattribs array, taken after code analysis has completed,
// before data analysis has begun.
private Anattrib[] mCodeOnlyAnattribs;
// Symbol lists loaded from platform symbol files. This is essentially a list
// of lists, of symbols.
private List PlatformSyms { get; set; }
// Extension script manager. Controls AppDomain sandbox.
private ScriptManager mScriptManager;
// All symbols, including user-defined, platform-specific, and auto-generated, keyed by
// label string. This is rebuilt whenever we do a refresh, and modified whenever
// labels or platform definitions are edited.
//
// Note this includes project/platform symbols that will not be in the assembled output.
public SymbolTable SymbolTable { get; private set; }
// Cross-reference data, indexed by file offset.
private Dictionary mXrefs = new Dictionary();
// Project and platform symbols that are being referenced from code.
public List ActiveDefSymbolList { get; private set; }
// List of messages, mostly problems detected during analysis.
public MessageList Messages { get; private set; }
#if DATA_PRESCAN
// Data scan results.
public TypedRangeSet RepeatedBytes { get; private set; }
public RangeSet StdAsciiBytes { get; private set; }
public RangeSet HighAsciiBytes { get; private set; }
#endif
// List of changes for undo/redo.
private List mUndoList = new List();
// Index of slot where next undo operation will be placed.
private int mUndoTop = 0;
// Index of top when the file was last saved.
private int mUndoSaveIndex = 0;
///
/// Constructs a new project.
///
public DisasmProject() { }
///
/// Prepares the object by instantiating various fields, some of which are sized to
/// match the length of the data file. The data file may not have been loaded yet
/// (e.g. when deserializing a project file).
///
public void Initialize(int fileDataLen) {
Debug.Assert(FileDataLength == 0); // i.e. Initialize() hasn't run yet
Debug.Assert(fileDataLen > 0);
FileDataLength = fileDataLen;
ProjectPathName = string.Empty;
AddrMap = new AddressMap(fileDataLen);
AddrMap.Set(0, 0x1000); // default load address to $1000; override later
// Default value is "no hint".
TypeHints = new CodeAnalysis.TypeHint[fileDataLen];
// Default value is "unspecified" for all bits.
StatusFlagOverrides = new StatusFlags[fileDataLen];
Comments = new string[fileDataLen];
// Populate with empty strings so we don't have to worry about null refs.
for (int i = 0; i < Comments.Length; i++) {
Comments[i] = string.Empty;
}
LongComments = new Dictionary();
Notes = new SortedList();
UserLabels = new Dictionary();
OperandFormats = new SortedList();
LvTables = new SortedList();
ProjectProps = new ProjectProperties();
SymbolTable = new SymbolTable();
PlatformSyms = new List();
ActiveDefSymbolList = new List();
Messages = new MessageList();
// Default to 65816. This will be replaced with value from project file or
// system definition.
ProjectProps.CpuType = CpuDef.CpuType.Cpu65816;
ProjectProps.IncludeUndocumentedInstr = false;
ProjectProps.TwoByteBrk = false;
UpdateCpuDef();
}
///
/// Discards resources, notably the sandbox AppDomain.
///
public void Cleanup() {
Debug.WriteLine("DisasmProject.Cleanup(): scriptMgr=" + mScriptManager);
if (mScriptManager != null) {
mScriptManager.Cleanup();
mScriptManager = null;
}
}
///
/// Prepares the DisasmProject for use as a new project.
///
/// 65xx data file contents.
/// Data file's filename (not pathname).
public void PrepForNew(byte[] fileData, string dataFileName) {
Debug.Assert(fileData.Length == FileDataLength);
mFileData = fileData;
DataFileName = dataFileName;
FileDataCrc32 = CommonUtil.CRC32.OnWholeBuffer(0, mFileData);
#if DATA_PRESCAN
ScanFileData();
#endif
// Mark the first byte as code so we have something to do. This may get
// overridden later.
TypeHints[0] = CodeAnalysis.TypeHint.Code;
}
///
/// Pulls items of interest out of the system definition object and applies them
/// to the project. Call this after LoadDataFile() for a new project.
///
/// Target system definition.
public void ApplySystemDef(SystemDef sysDef) {
CpuDef.CpuType cpuType = CpuDef.GetCpuTypeFromName(sysDef.Cpu);
bool includeUndoc = SystemDefaults.GetUndocumentedOpcodes(sysDef);
bool twoByteBrk = SystemDefaults.GetTwoByteBrk(sysDef);
CpuDef tmpDef = CpuDef.GetBestMatch(cpuType, includeUndoc, twoByteBrk);
// Store the best-matched CPU in properties, rather than whichever was originally
// requested. This way the behavior of the project is the same for everyone, even
// if somebody has a newer app version with specialized handling for the
// originally-specified CPU.
ProjectProps.CpuType = tmpDef.Type;
ProjectProps.IncludeUndocumentedInstr = includeUndoc;
ProjectProps.TwoByteBrk = twoByteBrk;
UpdateCpuDef();
ProjectProps.AnalysisParams.DefaultTextScanMode =
SystemDefaults.GetTextScanMode(sysDef);
ProjectProps.EntryFlags = SystemDefaults.GetEntryFlags(sysDef);
// Configure the load address.
if (SystemDefaults.GetFirstWordIsLoadAddr(sysDef) && mFileData.Length > 2) {
// First two bytes are the load address, code starts at offset +000002. We
// need to put the load address into the stream, but don't want it to get
// picked up as an address for something else. So we set it to the same
// address as the start of the file. The overlapping-address code should do
// the right thing with it.
//
// Updated after adding the "load address" report to the address edit dialog.
// We don't want the two-byte offset.
int loadAddr = RawData.GetWord(mFileData, 0, 2, false);
AddrMap.Set(0, loadAddr - 2);
AddrMap.Set(2, loadAddr);
OperandFormats[0] = FormatDescriptor.Create(2, FormatDescriptor.Type.NumericLE,
FormatDescriptor.SubType.None);
TypeHints[0] = CodeAnalysis.TypeHint.NoHint;
TypeHints[2] = CodeAnalysis.TypeHint.Code;
} else {
int loadAddr = SystemDefaults.GetLoadAddress(sysDef);
AddrMap.Set(0, loadAddr);
}
foreach (string str in sysDef.SymbolFiles) {
ProjectProps.PlatformSymbolFileIdentifiers.Add(str);
}
foreach (string str in sysDef.ExtensionScripts) {
ProjectProps.ExtensionScriptFileIdentifiers.Add(str);
}
}
public void UpdateCpuDef() {
CpuDef = CpuDef.GetBestMatch(ProjectProps.CpuType,
ProjectProps.IncludeUndocumentedInstr, ProjectProps.TwoByteBrk);
}
///
/// Sets the file CRC. Called during deserialization.
///
/// Data file CRC.
public void SetFileCrc(uint crc) {
Debug.Assert(FileDataLength > 0);
FileDataCrc32 = crc;
}
///
/// Sets the file data array. Used when the project is created from a project file.
///
/// 65xx data file contents.
/// Data file's filename (not pathname).
/// Reporting object for validation errors.
public void SetFileData(byte[] fileData, string dataFileName, ref FileLoadReport report) {
Debug.Assert(fileData.Length == FileDataLength);
Debug.Assert(CRC32.OnWholeBuffer(0, fileData) == FileDataCrc32);
mFileData = fileData;
DataFileName = dataFileName;
FixAndValidate(ref report);
#if DATA_PRESCAN
ScanFileData();
#endif
}
#if DATA_PRESCAN
private delegate bool ByteTest(byte val); // for ScanFileData()
///
/// Scans the contents of the file data array, noting runs of identical bytes and
/// other interesting bits.
///
/// The file data is guaranteed not to change, so doing a bit of work here can save
/// us time during data analysis.
///
private void ScanFileData() {
DateTime startWhen = DateTime.Now;
// Find runs of identical bytes.
TypedRangeSet repeats = new TypedRangeSet();
Debug.Assert(mFileData.Length > 0);
byte matchByte = mFileData[0];
int count = 1;
for (int i = 1; i < mFileData.Length; i++) {
if (mFileData[i] == matchByte) {
count++;
continue;
}
if (count >= DataAnalysis.MIN_RUN_LENGTH) {
repeats.AddRange(i - count, i - 1, matchByte);
}
matchByte = mFileData[i];
count = 1;
}
if (count >= DataAnalysis.MIN_RUN_LENGTH) {
repeats.AddRange(mFileData.Length - count, mFileData.Length - 1, matchByte);
}
RangeSet ascii = new RangeSet();
CreateByteRangeSet(ascii, mFileData, DataAnalysis.MIN_STRING_LENGTH,
delegate (byte val) {
return val >= 0x20 && val < 0x7f;
}
);
RangeSet highAscii = new RangeSet();
CreateByteRangeSet(highAscii, mFileData, DataAnalysis.MIN_STRING_LENGTH,
delegate (byte val) {
return val >= 0xa0 && val < 0xff;
}
);
if (false) {
repeats.DebugDump("Repeated-Bytes (" + DataAnalysis.MIN_RUN_LENGTH + "+)");
ascii.DebugDump("Standard-ASCII (" + DataAnalysis.MIN_STRING_LENGTH + "+)");
highAscii.DebugDump("High-ASCII (" + DataAnalysis.MIN_STRING_LENGTH + "+)");
}
Debug.WriteLine("ScanFileData took " +
((DateTime.Now - startWhen).TotalMilliseconds) + " ms");
RepeatedBytes = repeats;
StdAsciiBytes = ascii;
HighAsciiBytes = highAscii;
}
private void CreateByteRangeSet(RangeSet set, byte[] data, int minLen, ByteTest tester) {
int count = 0;
for (int i = 0; i < data.Length; i++) {
if (tester(data[i])) {
count++;
} else if (count < minLen) {
count = 0;
} else {
set.AddRange(i - count, i - 1);
count = 0;
}
}
if (count >= minLen) {
set.AddRange(data.Length - count, data.Length - 1);
}
}
#endif
///
/// Walks the list of format descriptors, fixing places where the data doesn't match.
/// This is run once, after the file is loaded.
///
private void FixAndValidate(ref FileLoadReport report) {
// Can't modify a list while we're iterating through it, so gather changes here.
Dictionary changes = new Dictionary();
foreach (KeyValuePair kvp in OperandFormats) {
FormatDescriptor dfd = kvp.Value;
// v1 project files specified string layouts as sub-types, and assumed they
// were high or low ASCII. Numeric values could use the ASCII sub-type, which
// included both high and low.
//
// v2 project files changed this to make string layouts types, with the
// character encoding specified in the sub-type. High and low ASCII became
// separate, explicitly specified items.
//
// When loading a v1 file, the old "Ascii" sub-type is deserialized to
// ASCII_GENERIC. Now that we have access to the file data, we need to refine
// the sub-type to high or low.
if (dfd.FormatSubType == FormatDescriptor.SubType.ASCII_GENERIC) {
FormatDescriptor newDfd;
if (dfd.IsString) {
// Determine the string encoding by looking at the first character.
// For some strings (StringL8, StringL16) we need to skip forward a
// byte or two. Empty strings with lengths or null-termination will
// be treated as low ASCII.
int checkOffset = kvp.Key;
if (dfd.FormatType == FormatDescriptor.Type.StringL8 && dfd.Length > 1) {
checkOffset++;
} else if (dfd.FormatType == FormatDescriptor.Type.StringL16 && dfd.Length > 2) {
checkOffset += 2;
}
bool isHigh = (FileData[checkOffset] & 0x80) != 0;
newDfd = FormatDescriptor.Create(dfd.Length, dfd.FormatType,
isHigh ? FormatDescriptor.SubType.HighAscii :
FormatDescriptor.SubType.Ascii);
} else if (dfd.IsNumeric) {
// This is a character constant in an instruction or data operand, such
// as ".dd1 'f'" or "LDA #'f'". Could be multi-byte (even instructions
// can be 16-bit). This is a little awkward, because at this point we
// can't tell the difference between instructions and data.
//
// However, we do know that instructions are always little-endian, that
// opcodes are one byte, that data values > $ff can't be ASCII encoded,
// and that $00 isn't a valid ASCII character. So we can apply the
// following test:
// - if the length is 1, it's data; grab the first byte
// - if it's NumericBE, it's data; grab the last byte
// - if the second byte is $00, it's data; grab the first byte
// - otherwise, it's an instruction; grab the second byte
int checkOffset;
if (dfd.FormatType == FormatDescriptor.Type.NumericBE) {
Debug.Assert(dfd.Length <= FormatDescriptor.MAX_NUMERIC_LEN);
checkOffset = kvp.Key + dfd.Length - 1;
} else if (dfd.Length < 2 || FileData[kvp.Key + 1] == 0x00) {
checkOffset = kvp.Key;
} else {
Debug.Assert(dfd.FormatType == FormatDescriptor.Type.NumericLE);
checkOffset = kvp.Key + 1;
}
bool isHigh = (FileData[checkOffset] & 0x80) != 0;
newDfd = FormatDescriptor.Create(dfd.Length, dfd.FormatType,
isHigh ? FormatDescriptor.SubType.HighAscii :
FormatDescriptor.SubType.Ascii);
} else {
Debug.Assert(false);
newDfd = dfd;
}
changes[kvp.Key] = newDfd;
Debug.WriteLine("Fix +" + kvp.Key.ToString("x6") + ": " +
dfd + " -> " + newDfd);
// possibly interesting, but rarely; very noisy
//report.Add(FileLoadItem.Type.Notice,
// "Fixed format at +" + kvp.Key.ToString("x6"));
}
}
// Run through the list again, this time looking for badly-formed strings. We're
// only checking structure, not character encoding, because you're allowed to have
// non-printable characters in strings.
foreach (KeyValuePair kvp in OperandFormats) {
FormatDescriptor dfd = kvp.Value;
if (dfd.IsString && !DataAnalysis.VerifyStringData(FileData, kvp.Key, dfd.Length,
dfd.FormatType, out string failMsg)) {
report.Add(FileLoadItem.Type.Warning,
"+" + kvp.Key.ToString("x6") + ": " + failMsg);
changes[kvp.Key] = null;
}
}
// Apply changes to main list.
foreach (KeyValuePair kvp in changes) {
if (kvp.Value == null) {
OperandFormats.Remove(kvp.Key);
} else {
OperandFormats[kvp.Key] = kvp.Value;
}
}
}
///
/// Loads platform symbol files and extension scripts.
///
/// Call this on initial load and whenever the set of platform symbol files changes
/// in the project config.
///
/// Failures here will be reported to the user but aren't fatal.
///
/// Multi-line string with all warnings from load process.
public string LoadExternalFiles() {
TaskTimer timer = new TaskTimer();
timer.StartTask("Total");
StringBuilder sb = new StringBuilder();
string projectDir = string.Empty;
if (!string.IsNullOrEmpty(ProjectPathName)) {
projectDir = Path.GetDirectoryName(ProjectPathName);
}
// Load the platform symbols first.
timer.StartTask("Platform Symbols");
PlatformSyms.Clear();
int loadOrdinal = 0;
foreach (string fileIdent in ProjectProps.PlatformSymbolFileIdentifiers) {
PlatformSymbols ps = new PlatformSymbols();
bool ok = ps.LoadFromFile(fileIdent, projectDir, loadOrdinal,
out FileLoadReport report);
if (ok) {
PlatformSyms.Add(ps);
}
if (report.Count > 0) {
sb.Append(report.Format());
}
loadOrdinal++;
}
timer.EndTask("Platform Symbols");
// Instantiate the script manager on first use.
timer.StartTask("Create ScriptManager");
if (mScriptManager == null) {
mScriptManager = new ScriptManager(this);
} else {
mScriptManager.Clear();
}
timer.EndTask("Create ScriptManager");
// Load the extension script files.
timer.StartTask("Load Extension Scripts");
foreach (string fileIdent in ProjectProps.ExtensionScriptFileIdentifiers) {
bool ok = mScriptManager.LoadPlugin(fileIdent, out FileLoadReport report);
if (report.Count > 0) {
sb.Append(report.Format());
}
}
timer.EndTask("Load Extension Scripts");
timer.EndTask("Total");
timer.DumpTimes("Time to load external files:");
return sb.ToString();
}
///
/// Checks some stuff. Problems are handled with assertions, so this is only
/// useful in debug builds.
///
public void Validate() {
// Confirm that we can walk through the file, stepping directly from the start
// of one thing to the start of the next. We won't normally do this, because
// we need to watch for embedded instructions.
int offset = 0;
while (offset < mFileData.Length) {
Anattrib attr = mAnattribs[offset];
bool thisIsCode = attr.IsInstructionStart;
Debug.Assert(attr.IsStart);
Debug.Assert(attr.Length != 0);
offset += attr.Length;
// Sometimes embedded instructions continue past the "outer" instruction,
// usually because we're misinterpreting the code. We need to deal with
// that here.
//
// One fun way to cause this is to have inline data from a plugin that got
// overwritten by the code analyzer. See test 2022 for an example.
int extraInstrBytes = 0;
while (offset < mFileData.Length && mAnattribs[offset].IsInstruction &&
!mAnattribs[offset].IsInstructionStart) {
extraInstrBytes++;
offset++;
}
// Make sure the extra code bytes were part of an instruction. Otherwise it
// means we moved from the end of a data area to the middle of an instruction,
// which is very bad.
Debug.Assert(extraInstrBytes == 0 || thisIsCode);
//if (extraInstrBytes > 0) { Debug.WriteLine("EIB=" + extraInstrBytes); }
// Max instruction len is 4, so the stray part must be shorter.
Debug.Assert(extraInstrBytes < 4);
}
Debug.Assert(offset == mFileData.Length);
// Confirm that all bytes are tagged as code, data, or inline data. The Asserts
// in Anattrib should confirm that nothing is tagged as more than one thing.
for (offset = 0; offset < mAnattribs.Length; offset++) {
Anattrib attr = mAnattribs[offset];
Debug.Assert(attr.IsInstruction || attr.IsInlineData || attr.IsData);
}
// Confirm that there are no Default format entries in OperandFormats.
foreach (KeyValuePair kvp in OperandFormats) {
Debug.Assert(kvp.Value.FormatType != FormatDescriptor.Type.Default);
Debug.Assert(kvp.Value.FormatType != FormatDescriptor.Type.REMOVE);
}
}
private void ValidateAddressMap() {
foreach (AddressMap.AddressMapEntry entry in AddrMap) {
if ((entry.Addr & 0xff0000) != ((entry.Addr + entry.Length - 1) & 0xff0000)) {
string fmt = Res.Strings.MSG_BANK_OVERRUN_DETAIL_FMT;
int firstNext = (entry.Addr & 0xff0000) + 0x010000;
int badOffset = entry.Offset + (firstNext - entry.Addr);
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Error,
entry.Offset,
MessageList.MessageEntry.MessageType.BankOverrun,
string.Format(fmt, "+" + badOffset.ToString("x6")),
MessageList.MessageEntry.ProblemResolution.None));
}
}
}
#region Analysis
///
/// Analyzes the file data. This is the main entry point for code/data analysis.
///
/// How much work to do.
/// Object to send debug output to.
/// Task timestamp collection object.
public void Analyze(UndoableChange.ReanalysisScope reanalysisRequired,
CommonUtil.DebugLog debugLog, TaskTimer reanalysisTimer) {
// This method doesn't report failures. It succeeds to the best of its ability,
// and handles problems by discarding bad data. The overall philosophy is that
// the program will never generate bad data, and any bad project file contents
// (possibly introduced by hand-editing) are identified at load time, called out
// to the user, and discarded.
//
// We do want to collect the failures so we can present them to the user.
Messages.Clear();
Debug.Assert(reanalysisRequired != UndoableChange.ReanalysisScope.None);
reanalysisTimer.StartTask("DisasmProject.Analyze()");
// Populate the symbol table with platform symbols, in file load order, then
// merge in the project symbols, potentially replacing platform symbols that
// have the same label. This version of the table is passed to plugins during
// code analysis.
reanalysisTimer.StartTask("SymbolTable init");
SymbolTable.Clear();
MergePlatformProjectSymbols();
// Merge user labels into the symbol table, overwriting platform/project symbols
// where they conflict. Labels whose values are out of sync (because of a change
// to the address map) are updated as part of this.
UpdateAndMergeUserLabels();
reanalysisTimer.EndTask("SymbolTable init");
if (reanalysisRequired == UndoableChange.ReanalysisScope.CodeAndData) {
// Always want to start with a blank array. Going to be lazy and let the
// system allocator handle that for us.
// NOTE: don't generate any Messages during code analysis -- we clear the
// list at the start of each pass, and we don't always analyze code.
mAnattribs = new Anattrib[mFileData.Length];
reanalysisTimer.StartTask("CodeAnalysis.Analyze");
CodeAnalysis ca = new CodeAnalysis(mFileData, CpuDef, mAnattribs, AddrMap,
TypeHints, StatusFlagOverrides, ProjectProps.EntryFlags,
ProjectProps.AnalysisParams, mScriptManager, debugLog);
ca.Analyze();
reanalysisTimer.EndTask("CodeAnalysis.Analyze");
// Save a copy of the current state.
mCodeOnlyAnattribs = new Anattrib[mAnattribs.Length];
Array.Copy(mAnattribs, mCodeOnlyAnattribs, mAnattribs.Length);
} else {
// Load Anattribs array from the stored copy.
Debug.WriteLine("Partial reanalysis");
reanalysisTimer.StartTask("CodeAnalysis (restore prev)");
Debug.Assert(mCodeOnlyAnattribs != null);
Array.Copy(mCodeOnlyAnattribs, mAnattribs, mAnattribs.Length);
reanalysisTimer.EndTask("CodeAnalysis (restore prev)");
}
reanalysisTimer.StartTask("Apply labels, formats, etc.");
// Apply any user-defined labels to the Anattribs array.
ApplyUserLabels(debugLog);
// Apply user-created format descriptors to instructions and data items.
ApplyFormatDescriptors(debugLog);
reanalysisTimer.EndTask("Apply labels, formats, etc.");
reanalysisTimer.StartTask("DataAnalysis");
DataAnalysis da = new DataAnalysis(this, mAnattribs);
da.DebugLog = debugLog;
reanalysisTimer.StartTask("DataAnalysis.AnalyzeDataTargets");
da.AnalyzeDataTargets();
reanalysisTimer.EndTask("DataAnalysis.AnalyzeDataTargets");
// Analyze uncategorized regions. When this completes, the Anattrib array will
// be complete for every offset, and the file will be traversible by walking
// through the lengths of each entry.
reanalysisTimer.StartTask("DataAnalysis.AnalyzeUncategorized");
da.AnalyzeUncategorized();
reanalysisTimer.EndTask("DataAnalysis.AnalyzeUncategorized");
reanalysisTimer.EndTask("DataAnalysis");
reanalysisTimer.StartTask("RemoveHiddenLabels");
RemoveHiddenLabels();
reanalysisTimer.EndTask("RemoveHiddenLabels");
// ----------
// NOTE: we could add an additional re-analysis entry point here, that just deals with
// platform symbols and xrefs, to be used after a change to project symbols. We'd
// need to check all existing refs to confirm that the symbol hasn't been removed.
// Symbol updates are sufficiently infrequent that this probably isn't worthwhile.
reanalysisTimer.StartTask("GenerateVariableRefs");
// Generate references to variables.
GenerateVariableRefs();
reanalysisTimer.EndTask("GenerateVariableRefs");
// NOTE: we could at this point apply platform address symbols as code labels, so
// that locations in the code that correspond to well-known addresses would pick
// up the appropriate label instead of getting auto-labeled. It's unclear
// whether this is desirable, especially if the user is planning to modify the
// output later on, and it could mess things up if we start slapping
// labels into the middle of data regions. It's generally safer to treat
// platform symbols as labels for constants and external references. If somebody
// finds an important use case we can revisit this; might merit a special type
// of equate or section in the platform symbol definition file.
reanalysisTimer.StartTask("GeneratePlatformSymbolRefs");
// Generate references to platform and project external symbols.
GeneratePlatformSymbolRefs();
reanalysisTimer.EndTask("GeneratePlatformSymbolRefs");
reanalysisTimer.StartTask("GenerateXrefs");
// Generate cross-reference lists.
mXrefs.Clear();
GenerateXrefs();
reanalysisTimer.EndTask("GenerateXrefs");
// replace simple auto-labels ("L1234") with annotated versions ("WR_1234")
if (ProjectProps.AutoLabelStyle != AutoLabel.Style.Simple) {
reanalysisTimer.StartTask("AnnotateAutoLabels");
AnnotateAutoLabels();
reanalysisTimer.EndTask("AnnotateAutoLabels");
}
reanalysisTimer.StartTask("GenerateActiveDefSymbolList");
// Generate the list of project/platform symbols that are being used. This forms
// the list of EQUates at the top of the file. The active set is identified from
// the cross-reference data.
GenerateActiveDefSymbolList();
reanalysisTimer.EndTask("GenerateActiveDefSymbolList");
#if DEBUG
reanalysisTimer.StartTask("Validate");
Validate();
reanalysisTimer.EndTask("Validate");
#endif
ValidateAddressMap();
reanalysisTimer.EndTask("DisasmProject.Analyze()");
//reanalysisTimer.DumpTimes("DisasmProject timers:", debugLog);
debugLog.LogI("Analysis complete");
//Problems.DebugDump();
Debug.WriteLine(SymbolTable.ToString());
}
///
/// Applies user labels to the Anattribs array. Symbols with stale Value fields will
/// be replaced.
///
/// Log for debug messages.
private void ApplyUserLabels(DebugLog genLog) {
foreach (KeyValuePair kvp in UserLabels) {
int offset = kvp.Key;
if (offset < 0 || offset >= mAnattribs.Length) {
genLog.LogE("Invalid offset +" + offset.ToString("x6") +
"(label=" + kvp.Value.Label + ")");
continue; // ignore this
}
if (mAnattribs[offset].Symbol != null) {
genLog.LogW("Multiple labels at offset +" + offset.ToString("x6") +
": " + kvp.Value.Label + " / " + mAnattribs[offset].Symbol.Label);
continue;
}
int expectedAddr = kvp.Value.Value;
Debug.Assert(expectedAddr == AddrMap.OffsetToAddress(offset));
// Add direct reference to the UserLabels Symbol object.
mAnattribs[offset].Symbol = kvp.Value;
}
}
///
/// Applies user-defined format descriptors to the Anattribs array. This specifies the
/// format for instruction operands, and identifies data items.
///
///
/// In an ideal world, this would be a trivial function. In practice it's possible for
/// all sorts of weird edge cases to arise, e.g. if you hint something as data, apply
/// formats, and then hint it as code, many strange things are possible. We don't want
/// to delete user data if it seems out of place, but we do want to ignore anything
/// that's going to confuse the source generator later on.
///
/// Problem reports are written to a log (which is shown by the Analyzer Output
/// window) and the Problems list. Once the latter is better established we can
/// stop sending them to the log.
///
/// Log for debug messages.
private void ApplyFormatDescriptors(DebugLog genLog) {
genLog.LogI("Applying format descriptors");
// TODO(someday): move error format strings to string dictionary
foreach (KeyValuePair kvp in OperandFormats) {
int offset = kvp.Key;
FormatDescriptor dfd = kvp.Value;
// Check offset.
if (offset < 0 || offset >= mFileData.Length) {
string msg = "invalid offset (desc=" + dfd + ")";
genLog.LogE("+" + offset.ToString("x6") + ": " + msg);
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Error,
offset,
MessageList.MessageEntry.MessageType.InvalidOffsetOrLength,
msg,
MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
Debug.Assert(false);
continue;
}
// Make sure it doesn't run off the end
if (offset + dfd.Length > mFileData.Length) {
string msg = "invalid offset+len: len=" + dfd.Length +
" file=" + mFileData.Length;
genLog.LogE("+" + offset.ToString("x6") + ": " + msg);
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Error,
offset,
MessageList.MessageEntry.MessageType.InvalidOffsetOrLength,
msg,
MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
Debug.Assert(false);
continue;
}
if (!AddrMap.IsContiguous(offset, dfd.Length)) {
string msg = "descriptor straddles address change; len=" + dfd.Length;
genLog.LogE("+" + offset.ToString("x6") + ": " + msg);
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Warning,
offset,
MessageList.MessageEntry.MessageType.InvalidOffsetOrLength,
msg,
MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
continue;
}
if (mAnattribs[offset].IsInstructionStart) {
// Check length for instruction formatters. This can happen if you format
// a bunch of bytes as single-byte data items and then add a code entry
// point.
if (dfd.Length != mAnattribs[offset].Length) {
string msg = "unexpected length on instr format descriptor (" +
dfd.Length + " vs " + mAnattribs[offset].Length + ")";
genLog.LogW("+" + offset.ToString("x6") + ": " + msg);
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Warning,
offset,
MessageList.MessageEntry.MessageType.InvalidOffsetOrLength,
msg,
MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
continue;
}
if (dfd.Length == 1) {
// No operand to format!
string msg = "unexpected format descriptor on single-byte op";
genLog.LogW("+" + offset.ToString("x6") + ": " + msg);
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Warning,
offset,
MessageList.MessageEntry.MessageType.InvalidDescriptor,
msg,
MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
continue;
}
if (!dfd.IsValidForInstruction) {
string msg = "descriptor not valid for instruction: " + dfd;
genLog.LogW("+" + offset.ToString("x6") + ": " + msg);
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Warning,
offset,
MessageList.MessageEntry.MessageType.InvalidDescriptor,
msg,
MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
continue;
}
} else if (mAnattribs[offset].IsInstruction) {
// Mid-instruction format.
string msg = "unexpected mid-instruction format descriptor";
genLog.LogW("+" + offset.ToString("x6") + ": " + msg);
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Warning,
offset,
MessageList.MessageEntry.MessageType.InvalidDescriptor,
msg,
MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
continue;
} else {
// Data or inline data. The data analyzer hasn't run yet. We want to
// confirm that the descriptor doesn't overlap with code.
//
// Data descriptors that overlap code are problematic, for two reasons.
// First, we end up with references to hidden labels, because the code that
// tries to prevent it sees an Anattrib with code at the target address and
// assumes all is well. Second, if the overlap ends partway into an
// instruction, an Anattrib-walker will move from a data region to the middle
// of an instruction, which should never happen.
//
// All instruction bytes have been marked, so we just need to confirm that
// none of the bytes spanned by this descriptor are instructions.
bool overlap = false;
for (int i = offset; i < offset + dfd.Length; i++) {
if (mAnattribs[i].IsInstruction) {
string msg =
"data format descriptor overlaps code at +" + i.ToString("x6");
genLog.LogW("+" + offset.ToString("x6") + ": " + msg);
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Warning,
offset,
MessageList.MessageEntry.MessageType.InvalidDescriptor,
msg,
MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
overlap = true;
break;
}
}
if (overlap) {
continue;
}
#if false
// Check junk+align directive. We don't outright reject the descriptor,
// because it still works as junk+unalign, but we want to add a warning
// message.
//
// NOTE: currently disabled because some relevant changes, such as editing
// a format descriptor sub-type, don't cause data re-analysis. I don't think
// this is important enough to special-case or implement elsewhere.
if (dfd.IsAlignedJunk) {
if (!AsmGen.GenCommon.CheckJunkAlign(offset, dfd, AddrMap)) {
string msg = "junk alignment not valid";
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Info,
offset,
MessageList.MessageEntry.MessageType.InvalidDescriptor,
msg,
MessageList.MessageEntry.ProblemResolution.None));
}
}
#endif
}
// All tests passed. Apply the descriptor.
mAnattribs[offset].DataDescriptor = dfd;
}
}
///
/// Merges symbols from PlatformSymbols and ProjectSymbols into SymbolTable.
///
/// This should be done before any other symbol assignment or generation, so that user
/// labels take precedence (by virtue of overwriting the earlier platform symbols),
/// and auto label generation can propery generate a unique label.
///
/// Within platform symbol loading, later symbols should replace earlier symbols,
/// so that ordering of platform files behaves in an intuitive fashion.
///
private void MergePlatformProjectSymbols() {
// Start by pulling in the platform symbols. The list in PlatformSymbols is in
// order, so we can just overwrite earlier symbols with matching labels.
foreach (PlatformSymbols ps in PlatformSyms) {
foreach (Symbol sym in ps) {
if (sym.Value == PlatformSymbols.ERASE_VALUE) {
// "erase" value
if (SymbolTable.TryGetValue(sym.Label, out Symbol found)) {
SymbolTable.Remove(found);
}
} else {
// overwrite
SymbolTable[sym.Label] = sym;
}
}
}
// Now add project symbols, overwriting platform symbols with the same label.
foreach (KeyValuePair kvp in ProjectProps.ProjectSyms) {
SymbolTable[kvp.Value.Label] = kvp.Value;
}
}
///
/// Returns true if a symbol with a matching label appears in the project symbols
/// or any of the platform lists. This operates on the raw lists, not SymbolTable.
///
///
/// Useful for determining whether a label masks a project or platform symbol.
///
private bool IsInProjectOrPlatformList(Symbol sym) {
if (sym == null) {
return false;
}
foreach (PlatformSymbols ps in PlatformSyms) {
if (ps.ContainsKey(sym.Label)) {
return true;
}
}
if (ProjectProps.ProjectSyms.ContainsKey(sym.Label)) {
return true;
}
return false;
}
///
/// Merges symbols from UserLabels into SymbolTable. Existing entries with matching
/// labels will be replaced.
///
///
/// It might make sense to exclude non-unique labels, but that's probably better done
/// with a UI filter option.
///
private void UpdateAndMergeUserLabels() {
// We store symbols as label+value, but for a user label the actual value is
// the address of the offset the label is associated with, which can change if
// the user updates the address map. It's convenient to store labels as Symbols
// because we also want the Type value, and it avoids having to create Symbol
// objects on the fly. If the value in the user label is wrong, we fix it here.
Dictionary changes = new Dictionary();
foreach (KeyValuePair kvp in UserLabels) {
int offset = kvp.Key;
Symbol sym = kvp.Value;
int expectedAddr = AddrMap.OffsetToAddress(offset);
if (sym.Value != expectedAddr) {
Symbol newSym = new Symbol(sym.Label, expectedAddr, sym.SymbolSource,
sym.SymbolType, sym.LabelAnno);
Debug.WriteLine("Updating label value: " + sym + " --> " + newSym);
changes[offset] = newSym;
sym = newSym;
}
SymbolTable[kvp.Value.Label] = sym;
}
// If we updated any symbols, merge the changes back into UserLabels.
if (changes.Count != 0) {
Debug.WriteLine("...merging " + changes.Count + " symbols into UserLabels");
}
foreach (KeyValuePair kvp in changes) {
UserLabels[kvp.Key] = kvp.Value;
}
}
///
/// Removes user labels from the symbol table if they're in the middle of an
/// instruction or multi-byte data area. (Easy way to cause this: hint a 3-byte
/// instruction as data, add a label to the middle byte, remove hints.)
///
/// Call this after the code and data analysis passes have completed. Any
/// references to the hidden labels will just fall through. It will be possible
/// to create multiple labels with the same name, because the app won't see them
/// in the symbol table.
///
private void RemoveHiddenLabels() {
// TODO(someday): keep the symbols in the symbol table so we can't create a
// duplicate, but flag it as hidden. The symbol resolver will need to know
// to ignore it. Provide a way for users to purge them. We could just blow
// them out of UserLabels right now, but I'm trying to avoid discarding user-
// created data without permission.
foreach (KeyValuePair kvp in UserLabels) {
int offset = kvp.Key;
if (!mAnattribs[offset].IsStart) {
Debug.WriteLine("Stripping hidden label '" + kvp.Value.Label + "'");
SymbolTable.Remove(kvp.Value);
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Warning,
offset,
MessageList.MessageEntry.MessageType.HiddenLabel,
kvp.Value.Label,
MessageList.MessageEntry.ProblemResolution.LabelIgnored));
}
}
}
///
/// Generates references to symbols in the local variable tables.
///
/// These only apply to instructions with a specific set of addressing modes.
///
/// This must be called after the code and data analysis passes have completed. It
/// should run before project/platform symbol references are generated, since we want
/// variables to take precedence.
///
/// This also adds all symbols in non-hidden variable tables to the main SymbolTable,
/// for the benefit of future uniqueness checks.
///
private void GenerateVariableRefs() {
LocalVariableLookup lvLookup = new LocalVariableLookup(LvTables, this, false);
for (int offset = 0; offset < FileData.Length; ) {
// Was a table defined at this offset?
List vars = lvLookup.GetVariablesDefinedAtOffset(offset);
if (vars != null) {
// All entries also get added to the main SymbolTable. This is a little
// wonky because the symbol might already exist with a different value.
// So long as the previous thing was also a variable, it doesn't matter.
foreach (DefSymbol defSym in vars) {
if (!SymbolTable.TryGetValue(defSym.Label, out Symbol sym)) {
// Symbol not yet in symbol table. Add it.
//
// NOTE: if you try to run the main app with uniqification enabled,
// this will cause the various uniquified forms of local variables
// to end up in the main symbol table. This can cause clashes with
// user labels that would not occur otherwise.
SymbolTable[defSym.Label] = defSym;
} else if (!sym.IsVariable) {
// Somehow we have a variable and a non-variable with the same
// name. Platform/project symbols haven't been processed yet, so
// this must be a clash with a user label. This could cause
// assembly source gen to fail later on. It's possible to do this
// by "hiding" a table and then adding a user label, so we can't just
// fix it at project load time.
//
// This is now handled by the LvLookup code, which renames the
// duplicate label, so we shouldn't get here.
Debug.WriteLine("Found non-variable with var name in symbol table: "
+ sym);
Debug.Assert(false);
}
}
} else if (LvTables.TryGetValue(offset, out LocalVariableTable unused)) {
// table was ignored
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Warning,
offset,
MessageList.MessageEntry.MessageType.HiddenLocalVariableTable,
string.Empty,
MessageList.MessageEntry.ProblemResolution.LocalVariableTableIgnored));
}
Anattrib attr = mAnattribs[offset];
if (attr.IsInstructionStart && attr.DataDescriptor == null) {
OpDef op = CpuDef.GetOpDef(FileData[offset]);
DefSymbol defSym = null;
if (op.IsDirectPageInstruction) {
Debug.Assert(attr.OperandAddress == FileData[offset + 1]);
defSym = lvLookup.GetSymbol(offset, FileData[offset + 1],
Symbol.Type.ExternalAddr);
} else if (op.IsStackRelInstruction) {
defSym = lvLookup.GetSymbol(offset, FileData[offset + 1],
Symbol.Type.Constant);
}
if (defSym != null) {
WeakSymbolRef vref = new WeakSymbolRef(defSym.Label,
WeakSymbolRef.Part.Low, op.IsStackRelInstruction ?
WeakSymbolRef.LocalVariableType.StackRelConst :
WeakSymbolRef.LocalVariableType.DpAddr);
mAnattribs[offset].DataDescriptor =
FormatDescriptor.Create(attr.Length, vref, false);
}
}
if (attr.IsDataStart || attr.IsInlineDataStart) {
offset += attr.Length;
} else {
// Advance by one, not attr.Length, so we don't miss embedded instructions.
offset++;
}
}
}
///
/// Generates references to symbols in the project/platform symbol tables.
///
/// For each instruction or data item that appears to reference an address, and
/// does not have a target offset, look for a matching address in the symbol tables.
///
/// This works pretty well for addresses, but is a little rough for constants.
///
/// Call this after the code and data analysis passes have completed. This doesn't
/// interact with labels, so the ordering there doesn't matter. This should come after
/// local variable resolution, so that those have priority.
///
private void GeneratePlatformSymbolRefs() {
bool checkNearby = ProjectProps.AnalysisParams.SeekNearbyTargets;
for (int offset = 0; offset < mAnattribs.Length; ) {
Anattrib attr = mAnattribs[offset];
Symbol sym;
int address;
OpDef.MemoryEffect accType = OpDef.MemoryEffect.Unknown;
if (attr.IsInstructionStart && attr.DataDescriptor == null &&
attr.OperandAddress >= 0 && attr.OperandOffset < 0) {
// This is an instruction that hasn't been explicitly formatted. It
// has an operand address, but not an offset, meaning it's a reference
// to an address outside the scope of the file. See if it has a
// platform symbol definition.
//
// It might seem unwise to examine the full symbol table, because it has
// non-project non-platform symbols in it. However, any matching user
// labels would have been applied already. Also, we want to ensure that
// conflicting user labels take precedence, e.g. creating a user label "COUT"
// will prevent a platform symbol with the same name from being visible.
// Using the full symbol table is potentially a tad less efficient than
// looking for a match exclusively in project/platform symbols, but it's
// the correct thing to do.
OpDef op = CpuDef.GetOpDef(FileData[offset]);
accType = op.MemEffect;
address = attr.OperandAddress;
sym = SymbolTable.FindNonVariableByAddress(address, accType);
} else if ((attr.IsDataStart || attr.IsInlineDataStart) &&
attr.DataDescriptor != null && attr.DataDescriptor.IsNumeric &&
attr.DataDescriptor.FormatSubType == FormatDescriptor.SubType.Address) {
// Found a Numeric/Address data item that matches. Data items don't have
// OperandAddress or OperandOffset set, so we need to check manually to
// see if the address falls within the project. In most situations this
// isn't really necessary, because the data analysis pass will have resolved
// interal references to auto-generated labels.
//
// This is only firing if the item is explicitly formatted as an
// Address, so we're essentially "upgrading" the user format.
address = RawData.GetWord(mFileData, offset, attr.DataDescriptor.Length,
attr.DataDescriptor.FormatType == FormatDescriptor.Type.NumericBE);
if (AddrMap.AddressToOffset(offset, address) < 0) {
accType = OpDef.MemoryEffect.ReadModifyWrite; // guess
sym = SymbolTable.FindNonVariableByAddress(address, accType);
} else {
Debug.WriteLine("Found unhandled internal data addr ref at +" +
offset.ToString("x6"));
address = -1; // don't touch interior stuff
sym = null;
}
} else {
address = -1;
sym = null;
}
if (address >= 0) {
// If we didn't find it, see if addr+1 has a label. Sometimes indexed
// addressing will use "STA addr-1,y". This will also catch "STA addr-1"
// when addr is the very start of a segment, which means we're actually
// finding a label reference rather than project/platform symbol; only
// works if the location already has a label.
if (sym == null && (address & 0xffff) < 0xffff && checkNearby) {
sym = SymbolTable.FindNonVariableByAddress(address + 1, accType);
if (sym != null && sym.SymbolSource != Symbol.Source.Project &&
sym.SymbolSource != Symbol.Source.Platform) {
Debug.WriteLine("Applying non-platform in GeneratePlatform: " + sym);
// should be okay to do this
}
}
// If we found something, and it's not a variable, create a descriptor.
if (sym != null && !sym.IsVariable) {
mAnattribs[offset].DataDescriptor =
FormatDescriptor.Create(mAnattribs[offset].Length,
new WeakSymbolRef(sym.Label, WeakSymbolRef.Part.Low), false);
}
}
if (attr.IsDataStart || attr.IsInlineDataStart) {
offset += attr.Length;
} else {
// Advance by one, not attr.Length, so we don't miss embedded instructions.
offset++;
}
}
}
///
/// Generates labels for branch and data targets, and xref lists for all referenced
/// offsets. Also generates Xref entries for DefSymbols (for .eq directives).
///
/// Call this after the code and data analysis passes have completed.
///
private void GenerateXrefs() {
// Xref generation. There are two general categories of references:
// (1) Numeric reference. Comes from instructions (e.g. "LDA $1000" or "BRA $1000")
// and Numeric/Address data items.
// (2) Symbolic reference. Comes from instructions and data with Symbol format
// descriptors. In some cases this may be a partial ref, e.g. "LDA #>label".
// The symbol's value may not match the operand, in which case an adjustment
// is applied.
//
// We want to tag both. So if "LDA $1000" becomes "LDA label-2", we want to
// add a numeric reference to the code at $1000, and a symbolic reference to the
// labe at $1002, that point back to the LDA instruction. These are presented
// slightly differently to the user. For a symbolic reference with no adjustment,
// we don't add the (redundant) numeric reference.
//
// In some cases the numeric reference will land in the middle of an instruction
// or multi-byte data area and won't be visible.
// Clear previous cross-reference data from project/platform symbols. These
// symbols don't have file offsets, so we can't store them in the main mXrefs
// list.
// TODO(someday): DefSymbol is otherwise immutable. We should put these elsewhere,
// maybe a Dictionary? Just mind the garbage collection.
foreach (Symbol sym in SymbolTable) {
if (sym is DefSymbol) {
(sym as DefSymbol).Xrefs.Clear();
}
}
// Create a mapping from label (which must be unique) to file offset. This
// is different from UserLabels (which only has user-created labels, and is
// sorted by offset) and SymbolTable (which has constants and platform symbols,
// and uses the address as value rather than the offset).
SortedList labelList = new SortedList(mFileData.Length,
Asm65.Label.LABEL_COMPARER);
for (int offset = 0; offset < mAnattribs.Length; offset++) {
Anattrib attr = mAnattribs[offset];
if (attr.Symbol != null) {
try {
labelList.Add(attr.Symbol.Label, offset);
} catch (ArgumentException ex) {
// Duplicate UserLabel entries are stripped when projects are loaded,
// but it might be possible to cause this by hiding/unhiding a
// label (e.g. using hints to place it in the middle of an instruction).
// Just ignore the duplicate.
Debug.WriteLine("Xref ignoring duplicate label '" + attr.Symbol.Label +
"': " + ex.Message);
}
}
}
LocalVariableLookup lvLookup = new LocalVariableLookup(LvTables, this, false);
// Walk through the Anattrib array, adding xref entries to things referenced
// by the entity at the current offset.
for (int offset = 0; offset < mAnattribs.Length; ) {
Anattrib attr = mAnattribs[offset];
XrefSet.XrefType xrefType = XrefSet.XrefType.Unknown;
OpDef.MemoryEffect accType = OpDef.MemoryEffect.Unknown;
if (attr.IsInstruction) {
OpDef op = CpuDef.GetOpDef(FileData[offset]);
if (op.IsSubroutineCall) {
xrefType = XrefSet.XrefType.SubCallOp;
} else if (op.IsBranchOrSubCall) {
xrefType = XrefSet.XrefType.BranchOp;
} else {
xrefType = XrefSet.XrefType.MemAccessOp;
accType = op.MemEffect;
}
} else if (attr.IsData || attr.IsInlineData) {
xrefType = XrefSet.XrefType.RefFromData;
}
bool hasZeroOffsetSym = false;
if (attr.DataDescriptor != null) {
FormatDescriptor dfd = attr.DataDescriptor;
if (dfd.FormatSubType == FormatDescriptor.SubType.Symbol) {
// For instructions with address operands that resolve in-file, grab
// the target offset.
int operandOffset = -1;
if (attr.IsInstructionStart) {
operandOffset = attr.OperandOffset;
}
// Is this a reference to a label?
if (labelList.TryGetValue(dfd.SymbolRef.Label, out int symOffset)) {
// Compute adjustment.
int adj = 0;
if (operandOffset >= 0) {
// We can compute (symOffset - operandOffset), but that gives us
// the offset adjustment, not the address adjustment.
adj = mAnattribs[symOffset].Address -
mAnattribs[operandOffset].Address;
}
AddXref(symOffset,
new XrefSet.Xref(offset, true, xrefType, accType, adj));
if (adj == 0) {
hasZeroOffsetSym = true;
}
} else if (dfd.SymbolRef.IsVariable) {
DefSymbol defSym = lvLookup.GetSymbol(offset, dfd.SymbolRef);
if (defSym != null) {
int adj = 0;
if (operandOffset >= 0) {
adj = defSym.Value - operandOffset;
}
defSym.Xrefs.Add(
new XrefSet.Xref(offset, true, xrefType, accType, adj));
}
} else if (SymbolTable.TryGetValue(dfd.SymbolRef.Label, out Symbol sym)) {
// Is this a reference to a project/platform symbol?
if (sym.SymbolSource == Symbol.Source.Project ||
sym.SymbolSource == Symbol.Source.Platform) {
DefSymbol defSym = sym as DefSymbol;
int adj = 0;
if (operandOffset >= 0) {
adj = defSym.Value - operandOffset;
}
defSym.Xrefs.Add(
new XrefSet.Xref(offset, true, xrefType, accType, adj));
} else {
// Can get here if somebody creates an address operand symbol
// that refers to a local variable.
Debug.WriteLine("NOTE: not xrefing +" + offset.ToString("x6") +
" " + sym);
}
} else {
// Reference to non-existent symbol.
Messages.Add(new MessageList.MessageEntry(
MessageList.MessageEntry.SeverityLevel.Info,
offset,
MessageList.MessageEntry.MessageType.UnresolvedWeakRef,
dfd.SymbolRef.Label,
MessageList.MessageEntry.ProblemResolution.FormatDescriptorIgnored));
}
} else if (dfd.FormatSubType == FormatDescriptor.SubType.Address) {
// not expecting this format on an instruction operand
Debug.Assert(attr.IsData || attr.IsInlineData);
int operandOffset = RawData.GetWord(mFileData, offset,
dfd.Length, dfd.FormatType == FormatDescriptor.Type.NumericBE);
AddXref(operandOffset,
new XrefSet.Xref(offset, false, xrefType, accType, 0));
}
// Look for instruction offset references. We skip this if we've already
// added a reference from a symbol with zero adjustment, since that would
// just leave a duplicate entry. (The symbolic ref wins because we need
// it for the label localizer and possibly the label refactorer.)
if (!hasZeroOffsetSym && attr.IsInstructionStart && attr.OperandOffset >= 0) {
AddXref(attr.OperandOffset,
new XrefSet.Xref(offset, false, xrefType, accType, 0));
}
}
if (attr.IsDataStart) {
// There shouldn't be data items inside of other data items.
offset += attr.Length;
} else {
// Advance by one, not attr.Length, so we don't miss embedded instructions.
offset++;
}
}
}
///
/// Adds an Xref entry to an XrefSet. The XrefSet will be created if necessary.
///
/// File offset for which cross-references are being noted.
/// Cross reference to add to the set.
private void AddXref(int offset, XrefSet.Xref xref) {
if (!mXrefs.TryGetValue(offset, out XrefSet xset)) {
xset = mXrefs[offset] = new XrefSet();
}
xset.Add(xref);
}
///
/// Returns the XrefSet for the specified offset. May return null if the set is
/// empty.
///
public XrefSet GetXrefSet(int offset) {
mXrefs.TryGetValue(offset, out XrefSet xset);
return xset; // will be null if not found
}
///
/// Replaces generic auto-labels with fancier versions generated from xrefs.
///
private void AnnotateAutoLabels() {
AutoLabel.Style style = ProjectProps.AutoLabelStyle;
Debug.Assert(style != AutoLabel.Style.Simple);
for (int offset = 0; offset < mAnattribs.Length; offset++) {
Anattrib attr = mAnattribs[offset];
if (attr.Symbol != null && attr.Symbol.SymbolSource == Symbol.Source.Auto) {
XrefSet xset = GetXrefSet(offset);
if (xset == null) {
// Nothing useful to do here. This is unexpected, since auto-labels
// should only exist because something referenced the offset.
continue;
}
Symbol newSym =
AutoLabel.GenerateAnnotatedLabel(attr.Address, SymbolTable, xset, style);
if (!newSym.Equals(attr.Symbol)) {
//Debug.WriteLine("Replace " + attr.Symbol.Label + " with " +newSym.Label);
// Replace the symbol in Anattribs, update the symbol table, then
// call Refactor to update everything that referenced it.
Symbol oldSym = mAnattribs[offset].Symbol;
mAnattribs[offset].Symbol = newSym;
SymbolTable.Remove(oldSym);
SymbolTable.Add(newSym);
RefactorLabel(offset, oldSym.Label);
}
}
}
}
///
/// Generates the list of project/platform symbols that are being used. Any
/// DefSymbol with a non-empty Xrefs is included. Previous contents are cleared.
///
/// The list is sorted primarily by value, secondarily by symbol name, with constants
/// appearing before addresses.
///
/// Call this after Xrefs are generated.
///
private void GenerateActiveDefSymbolList() {
ActiveDefSymbolList.Clear();
foreach (Symbol sym in SymbolTable) {
if (!(sym is DefSymbol) || sym.IsVariable) {
continue;
}
DefSymbol defSym = sym as DefSymbol;
if (defSym.Xrefs.Count == 0) {
continue;
}
ActiveDefSymbolList.Add(defSym);
}
// Sort order:
// - constants appear before addresses
// - ascending numeric value
// - ascending label
ActiveDefSymbolList.Sort(delegate (DefSymbol a, DefSymbol b) {
// Put constants first.
int ca = (a.IsConstant) ? 1 : 0;
int cb = (b.IsConstant) ? 1 : 0;
if (ca != cb) {
return cb - ca;
}
if (a.Value < b.Value) {
return -1;
} else if (a.Value > b.Value) {
return 1;
}
return Asm65.Label.LABEL_COMPARER.Compare(a.Label, b.Label);
});
}
#endregion Analysis
#region Change Management
///
/// Generates a ChangeSet that merges the FormatDescriptors in the new list into
/// OperandFormats.
///
/// All existing descriptors that overlap with new descriptors will be removed.
/// In cases where old and new descriptors have the same starting offset, this
/// will be handled with a single change object.
///
/// If old and new descriptors are identical, no change object will be generated.
/// It's possible for this to return an empty change set.
///
/// List of new format descriptors.
/// Change set.
public ChangeSet GenerateFormatMergeSet(SortedList newList) {
Debug.WriteLine("Generating format merge set...");
ChangeSet cs = new ChangeSet(newList.Count * 2);
// The Keys and Values properties are documented to return the internal data
// structure, not make a copy, so this will be fast.
IList mainKeys = OperandFormats.Keys;
IList mainValues = OperandFormats.Values;
IList newKeys = newList.Keys;
IList newValues = newList.Values;
// The basic idea is to walk through the new list, checking each entry for
// conflicts with the main list. If there's no conflict, we create a change
// object for the new item. If there is a conflict, we resolve it appropriately.
//
// The check on the main list is very fast because both lists are in sorted
// order, so we can just walk the main list forward. If a main-list entry
// conflicts, we create a removal object, and advance the main index.
int mainIndex = 0;
int newIndex = 0;
while (newIndex < newKeys.Count) {
int newOffset = newKeys[newIndex];
int newLength = newValues[newIndex].Length;
if (mainIndex >= mainKeys.Count) {
// We've run off the end of the main list. Just add the new item.
UndoableChange uc = UndoableChange.CreateActualOperandFormatChange(
newOffset, null, newValues[newIndex]);
cs.AddNonNull(uc);
newIndex++;
continue;
}
// Check for overlap by computing the intersection. Start and end form two
// points; the intersection is the largest of the start points and the
// smallest of the end points. If the result of the computation puts end before
// start, there's no overlap.
int mainOffset = mainKeys[mainIndex];
int mainLength = mainValues[mainIndex].Length;
Debug.Assert(newLength > 0 && mainLength > 0);
int interStart = Math.Max(mainOffset, newOffset);
int interEnd = Math.Min(mainOffset + mainLength, newOffset + newLength);
// exclusive end point, so interEnd == interStart means no overlap
if (interEnd > interStart) {
Debug.WriteLine("Found overlap: main(+" + mainOffset.ToString("x6") +
"," + mainLength + ") : new(+" + newOffset.ToString("x6") +
"," + newLength + ")");
// See if the initial offsets are identical. If so, put the add and
// remove into a single change. This isn't strictly necessary, but it's
// slightly more efficient.
if (mainOffset == newOffset) {
// Check to see if the descriptors are identical. If so, ignore this.
if (mainValues[mainIndex] == newValues[newIndex]) {
Debug.WriteLine(" --> no-op change " + newValues[newIndex]);
} else {
Debug.WriteLine(" --> replace change " + newValues[newIndex]);
UndoableChange uc = UndoableChange.CreateActualOperandFormatChange(
newOffset, mainValues[mainIndex], newValues[newIndex]);
cs.AddNonNull(uc);
}
} else {
// Remove the old entry, add the new entry.
Debug.WriteLine(" --> remove/add change " + newValues[newIndex]);
UndoableChange ruc = UndoableChange.CreateActualOperandFormatChange(
mainOffset, mainValues[mainIndex], null);
UndoableChange auc = UndoableChange.CreateActualOperandFormatChange(
newOffset, null, newValues[newIndex]);
cs.AddNonNull(ruc);
cs.AddNonNull(auc);
}
newIndex++;
// Remove all other main-list entries that overlap with this one.
while (++mainIndex < mainKeys.Count) {
mainOffset = mainKeys[mainIndex];
mainLength = mainValues[mainIndex].Length;
interStart = Math.Max(mainOffset, newOffset);
interEnd = Math.Min(mainOffset + mainLength, newOffset + newLength);
// exclusive end point, so interEnd == interStart means no overlap
if (interEnd <= interStart) {
break;
}
Debug.WriteLine(" also remove +" + mainOffset.ToString("x6") +
mainValues[mainIndex]);
UndoableChange uc = UndoableChange.CreateActualOperandFormatChange(
mainOffset, mainValues[mainIndex], null);
cs.AddNonNull(uc);
}
} else {
// No overlap. If the main entry is earlier, we can cross it off the list
// and advance to the next one. Otherwise, we add the change and advance
// that list.
if (mainOffset < newOffset) {
mainIndex++;
} else {
Debug.WriteLine("Add non-overlap " + newOffset.ToString("x6") +
newValues[newIndex]);
UndoableChange uc = UndoableChange.CreateActualOperandFormatChange(
newOffset, null, newValues[newIndex]);
cs.AddNonNull(uc);
newIndex++;
}
}
}
// Trim away excess capacity, since this will probably be sitting in an undo
// list for a long time.
cs.TrimExcess();
Debug.WriteLine("Total " + cs.Count + " changes");
return cs;
}
///
/// Returns the analyzer attributes for the specified byte offset.
///
/// Bear in mind that Anattrib is a struct, and thus the return value is a copy.
///
public Anattrib GetAnattrib(int offset) {
return mAnattribs[offset];
}
///
/// Returns true if the offset has a long comment or note. Used for determining how to
/// split up a data area. Currently not returning true for an end-of-line comment.
///
/// Offset of interest.
/// True if a comment or note was found.
public bool HasCommentOrNote(int offset) {
return (LongComments.ContainsKey(offset) ||
Notes.ContainsKey(offset));
}
///
/// True if an "undo" operation is available.
///
public bool CanUndo { get { return mUndoTop > 0; } }
///
/// True if a "redo" operation is available.
///
public bool CanRedo { get { return mUndoTop < mUndoList.Count; } }
///
/// True if something has changed since the last time the file was saved.
///
public bool IsDirty { get { return mUndoTop != mUndoSaveIndex; } }
///
/// Sets the save index equal to the undo position. Do this after the file has
/// been successfully saved.
///
public void ResetDirtyFlag() {
mUndoSaveIndex = mUndoTop;
}
///
/// Returns the next undo operation, and moves the pointer to the previous item.
///
public ChangeSet PopUndoSet() {
if (!CanUndo) {
throw new Exception("Can't undo");
}
Debug.WriteLine("PopUndoSet: returning entry " + (mUndoTop - 1) + ": " +
mUndoList[mUndoTop - 1]);
return mUndoList[--mUndoTop];
}
///
/// Returns the next redo operation, and moves the pointer to the next item.
///
///
public ChangeSet PopRedoSet() {
if (!CanRedo) {
throw new Exception("Can't redo");
}
Debug.WriteLine("PopRedoSet: returning entry " + mUndoTop + ": " +
mUndoList[mUndoTop]);
return mUndoList[mUndoTop++];
}
///
/// Adds a change set to the undo list. All redo operations above it on the
/// stack are removed.
///
/// We currently allow empty sets.
///
/// Set to push.
public void PushChangeSet(ChangeSet changeSet) {
if (IsReadOnly) {
return;
}
Debug.WriteLine("PushChangeSet: adding " + changeSet);
// Remove all of the "redo" entries from the current position to the end.
if (mUndoTop < mUndoList.Count) {
Debug.WriteLine("PushChangeSet: removing " + (mUndoList.Count - mUndoTop) +
" entries");
mUndoList.RemoveRange(mUndoTop, mUndoList.Count - mUndoTop);
}
mUndoList.Add(changeSet);
mUndoTop = mUndoList.Count;
// If the user makes a change, saves the file, hits undo, then makes another change,
// the "undo top" and "save index" will be equal, which will make us think the
// file doesn't need to be saved. In reality there is no longer any undo index that
// matches the saved file state.
if (mUndoSaveIndex >= mUndoTop) {
mUndoSaveIndex = -1;
}
}
///
/// Returns the change at the top of the list, i.e. the one that would be popped off
/// if we hit "undo".
///
/// The change set. The caller must not modify it.
public ChangeSet GetTopChange() {
Debug.Assert(mUndoList.Count > 0);
Debug.Assert(mUndoTop > 0);
return mUndoList[mUndoTop - 1];
}
///
/// Generate undo/redo history, for the debug window.
///
public string DebugGetUndoRedoHistory() {
StringBuilder sb = new StringBuilder();
sb.Append("Bracketed change will be overwritten by next action\r\n\r\n");
for (int i = 0; i < mUndoList.Count; i++) {
ChangeSet cs = mUndoList[i];
char lbr, rbr;
if (i == mUndoTop) {
lbr = '[';
rbr = ']';
} else {
lbr = rbr = ' ';
}
sb.AppendFormat("{0}{3,3:D}{1}{2}: {4} change{5}\r\n",
lbr, rbr, i == mUndoSaveIndex ? "*" : " ",
i, cs.Count, cs.Count == 1 ? "" : "s");
for (int j = 0; j < cs.Count; j++) {
UndoableChange uc = cs[j];
sb.AppendFormat(" type={0} offset=+{1} reReq={2}\r\n",
uc.Type, uc.HasOffset ? uc.Offset.ToString("x6") : "N/A",
uc.ReanalysisRequired);
}
}
if (mUndoTop == mUndoList.Count) {
sb.AppendFormat("[ - ]{0}\r\n", mUndoTop == mUndoSaveIndex ? "*" : " ");
}
return sb.ToString();
}
///
/// Applies the changes to the project, and updates the display.
///
/// Set of changes to apply.
/// If set, undo the changes instead.
/// List of offsets affected by change. Only meaningful
/// when the result is not "None".
/// An indication of the level of reanalysis required. If this returns None,
/// the list of offsets to update will be in affectedOffsets.
public UndoableChange.ReanalysisScope ApplyChanges(ChangeSet cs, bool backward,
out RangeSet affectedOffsets) {
affectedOffsets = new RangeSet();
if (IsReadOnly) {
return UndoableChange.ReanalysisScope.None;
}
UndoableChange.ReanalysisScope needReanalysis = UndoableChange.ReanalysisScope.None;
// TODO(maybe): if changes overlap, we need to apply them in reverse order when
// "backward" is set. This requires a reverse-order enumerator from
// ChangeSet. Not currently needed.
foreach (UndoableChange uc in cs) {
object oldValue, newValue;
// Unpack change, flipping old/new for undo.
if (!backward) {
oldValue = uc.OldValue;
newValue = uc.NewValue;
} else {
oldValue = uc.NewValue;
newValue = uc.OldValue;
}
int offset = uc.Offset;
switch (uc.Type) {
case UndoableChange.ChangeType.Dummy:
//if (uc.ReanalysisRequired == UndoableChange.ReanalysisFlags.None) {
// affectedOffsets.AddRange(0, FileData.Length - 1);
//}
break;
case UndoableChange.ChangeType.SetAddress: {
AddressMap addrMap = AddrMap;
if (addrMap.Get(offset) != (int)oldValue) {
Debug.WriteLine("GLITCH: old address value mismatch (" +
addrMap.Get(offset) + " vs " + (int)oldValue + ")");
Debug.Assert(false);
}
addrMap.Set(offset, (int)newValue);
Debug.WriteLine("Map offset +" + offset.ToString("x6") + " to $" +
((int)newValue).ToString("x6"));
// ignore affectedOffsets
Debug.Assert(uc.ReanalysisRequired ==
UndoableChange.ReanalysisScope.CodeAndData);
}
break;
case UndoableChange.ChangeType.SetTypeHint: {
// Always requires full code+data re-analysis.
ApplyTypeHints((TypedRangeSet)oldValue, (TypedRangeSet)newValue);
// ignore affectedOffsets
Debug.Assert(uc.ReanalysisRequired ==
UndoableChange.ReanalysisScope.CodeAndData);
}
break;
case UndoableChange.ChangeType.SetStatusFlagOverride: {
if (StatusFlagOverrides[offset] != (StatusFlags)oldValue) {
Debug.WriteLine("GLITCH: old status flag mismatch (" +
StatusFlagOverrides[offset] + " vs " +
(StatusFlags)oldValue + ")");
Debug.Assert(false);
}
StatusFlagOverrides[offset] = (StatusFlags)newValue;
// ignore affectedOffsets
Debug.Assert(uc.ReanalysisRequired ==
UndoableChange.ReanalysisScope.CodeAndData);
}
break;
case UndoableChange.ChangeType.SetLabel: {
// NOTE: this is about managing changes to UserLabels. Adding
// or removing a user-defined label requires a full reanalysis,
// even if there was already an auto-generated label present,
// so we don't need to undo/redo Anattribs for anything except
// for renaming a user-defined label.
UserLabels.TryGetValue(offset, out Symbol oldSym);
if (oldSym != (Symbol) oldValue) {
Debug.WriteLine("GLITCH: old label value mismatch ('" +
oldSym + "' vs '" + oldValue + "')");
Debug.Assert(false);
}
if (newValue == null) {
// We're removing a user label.
UserLabels.Remove(offset);
SymbolTable.Remove((Symbol)oldValue); // unnecessary? will regen
Debug.Assert(uc.ReanalysisRequired ==
UndoableChange.ReanalysisScope.DataOnly);
} else {
// We're adding or renaming a user label.
//
// We should not be changing a label to the same value as an
// existing label -- the dialog should have prevented it.
// This is important because, if we edit a label to match an
// auto-generated label, we'll have a duplicate label unless we
// do a full code+data reanalysis. If we're okay with reanalyzing
// on user-label renames, we can allow such conflicts.
//
// We might be changing it to match an existing platform symbol
// though. (Ex: create label FOO, add .sym65 with symbol FOO,
// edit FOO to BAR, then hit undo.)
if (oldValue != null) {
SymbolTable.Remove((Symbol)oldValue);
}
UserLabels[offset] = (Symbol)newValue;
SymbolTable[((Symbol)newValue).Label] = (Symbol)newValue;
//SymbolTable.Add((Symbol)newValue);
Debug.Assert(oldSym != null || uc.ReanalysisRequired ==
UndoableChange.ReanalysisScope.DataOnly);
}
if (uc.ReanalysisRequired == UndoableChange.ReanalysisScope.None) {
// Shouldn't really be "changing" from null to null, but
// it's legal, so don't blow up if it happens.
// (The assert on SymbolSource is older -- we now only care about
// what's in UserLabels, which are always Source=User.)
Debug.Assert((oldValue == null && newValue == null) ||
(((Symbol)oldValue).SymbolSource == Symbol.Source.User &&
((Symbol)newValue).SymbolSource == Symbol.Source.User));
// Not doing a full refresh, so keep this up to date.
mAnattribs[offset].Symbol = (Symbol)newValue;
if (oldValue != null) {
// Update everything in Anattribs and OperandFormats that
// referenced the old symbol.
RefactorLabel(offset, ((Symbol)oldValue).Label);
}
// Compute the affected offsets. We have one special case to
// consider: if we renamed a label, and the old or new name is
// in project or platform symbols, we need to restore that
// symbol to the symbol table. The most reliable way to do that
// is to switch us to a data re-analysis.
//
// The UI doesn't let you directly edit a label to overwrite a
// symbol, but see FOO/BAR example above.
if (IsInProjectOrPlatformList((Symbol)oldValue) ||
IsInProjectOrPlatformList((Symbol)newValue)) {
Debug.WriteLine("Label change masked/unmasked " +
"project/platform symbol");
needReanalysis |= UndoableChange.ReanalysisScope.DataOnly;
} else {
affectedOffsets.Add(offset);
// Use the cross-reference table to identify the offsets that
// we need to update.
if (mXrefs.TryGetValue(offset, out XrefSet xrefs)) {
foreach (XrefSet.Xref xr in xrefs) {
// This isn't quite right -- in theory we should be
// adding all offsets that are part of the instruction,
// so that affectedOffsets can hold a contiguous range
// instead of a collection of opcode offsets. In
// practice, for a label change, it shouldn't matter.
affectedOffsets.Add(xr.Offset);
}
}
}
} else {
// We're not calling RefactorLabel() here because we should
// only be doing the reanalysis if we're adding or removing
// the label, not renaming it. If that changes, we'll need
// to do the refactor here, though we can skip Anattribs work.
Debug.Assert(oldValue == null || newValue == null);
}
// For add/edit/remove, we need to see if what we do will impact
// the behavior of a plugin. We don't need to do this on
// project/platform symbol changes because project property changes
// always update code and data.
if (mScriptManager.IsLabelSignificant((Symbol)oldValue,
(Symbol)newValue)) {
Debug.WriteLine("Plugin claims symbol is significant");
needReanalysis |= UndoableChange.ReanalysisScope.CodeAndData;
}
}
break;
case UndoableChange.ChangeType.SetOperandFormat: {
// Note this is used for data/inline-data as well as instructions.
OperandFormats.TryGetValue(offset, out FormatDescriptor current);
if (current != (FormatDescriptor)oldValue) {
Debug.WriteLine("GLITCH: old operand format mismatch (" +
current + " vs " + oldValue + ")");
Debug.Assert(false);
}
if (newValue == null) {
OperandFormats.Remove(offset);
mAnattribs[offset].DataDescriptor = null;
} else {
OperandFormats[offset] = mAnattribs[offset].DataDescriptor =
(FormatDescriptor)newValue;
}
if (uc.ReanalysisRequired == UndoableChange.ReanalysisScope.None) {
// Add every offset in the range. The length might be changing
// (e.g. an offset with a single byte is now the start of a
// 10-byte string), so touch everything that was affected by
// the old descriptor or is affected by the new descriptor.
// [This may no longer be necessary -- size changes now
// require reanalysis.]
int afctLen = 1;
if (oldValue != null) {
afctLen =
Math.Max(afctLen, ((FormatDescriptor)oldValue).Length);
}
if (newValue != null) {
afctLen =
Math.Max(afctLen, ((FormatDescriptor)newValue).Length);
}
for (int i = offset; i < offset + afctLen; i++) {
affectedOffsets.Add(i);
}
}
}
break;
case UndoableChange.ChangeType.SetComment: {
if (!Comments[offset].Equals(oldValue)) {
Debug.WriteLine("GLITCH: old comment value mismatch ('" +
Comments[offset] + "' vs '" + oldValue + "')");
Debug.Assert(false);
}
Comments[offset] = (string)newValue;
// Only affects this offset.
affectedOffsets.Add(offset);
}
break;
case UndoableChange.ChangeType.SetLongComment: {
LongComments.TryGetValue(offset, out MultiLineComment current);
if (current != (MultiLineComment)oldValue) {
Debug.WriteLine("GLITCH: old long comment value mismatch ('" +
current + "' vs '" + oldValue + "')");
Debug.Assert(false);
}
if (newValue == null) {
LongComments.Remove(offset);
} else {
LongComments[offset] = (MultiLineComment)newValue;
}
// Only affects this offset.
affectedOffsets.Add(offset);
}
break;
case UndoableChange.ChangeType.SetNote: {
Notes.TryGetValue(offset, out MultiLineComment current);
if (current != (MultiLineComment)oldValue) {
Debug.WriteLine("GLITCH: old note value mismatch ('" +
current + "' vs '" + oldValue + "')");
Debug.Assert(false);
}
if (newValue == null) {
Notes.Remove(offset);
} else {
Notes[offset] = (MultiLineComment)newValue;
}
// Only affects this offset.
affectedOffsets.Add(offset);
}
break;
case UndoableChange.ChangeType.SetProjectProperties: {
bool needExternalFileReload = !CommonUtil.Container.StringListEquals(
((ProjectProperties)oldValue).PlatformSymbolFileIdentifiers,
((ProjectProperties)newValue).PlatformSymbolFileIdentifiers,
null /*StringComparer.InvariantCulture*/);
needExternalFileReload |= !CommonUtil.Container.StringListEquals(
((ProjectProperties)oldValue).ExtensionScriptFileIdentifiers,
((ProjectProperties)newValue).ExtensionScriptFileIdentifiers,
null);
// ProjectProperties are mutable, so create a new object that's
// a clone of the one that will live in the undo buffer.
ProjectProps = new ProjectProperties((ProjectProperties)newValue);
// Most of the properties are simply used during the reanalysis
// process. This must be set explicitly. NOTE: replacing this
// could cause cached data (such as Formatter strings) to be
// discarded, so ideally we wouldn't do this unless we know the
// CPU definition has changed (or we know that GetBestMatch is
// memoizing results and will return the same object).
Debug.WriteLine("Replacing CPU def object");
UpdateCpuDef();
if (needExternalFileReload) {
string errMsgs = LoadExternalFiles();
// TODO(someday): if the plugin failed to compile, we will have
// one or more error messages, which we are currently discarding
// because we can't create UI here. We either need a "change
// messages" feature, or we need to pre-flight the plugin and
// report the failure elsewhere. (We also want a manual
// "reload all external files and plugins" command, which might
// run through here.)
}
}
// ignore affectedOffsets
Debug.Assert(uc.ReanalysisRequired ==
UndoableChange.ReanalysisScope.CodeAndData);
break;
case UndoableChange.ChangeType.SetLocalVariableTable: {
LvTables.TryGetValue(offset, out LocalVariableTable current);
if (current != (LocalVariableTable)oldValue) {
Debug.WriteLine("GLITCH: old lvt value mismatch: current=" +
current + " old=" + (LocalVariableTable)oldValue);
Debug.Assert(false);
}
if (newValue == null) {
LvTables.Remove(offset);
} else {
LvTables[offset] = (LocalVariableTable)newValue;
}
// ignore affectedOffsets
Debug.Assert(uc.ReanalysisRequired ==
UndoableChange.ReanalysisScope.DataOnly);
}
break;
default:
break;
}
needReanalysis |= uc.ReanalysisRequired;
}
return needReanalysis;
}
///
/// Updates all symbolic references to the old label. Call this after replacing
/// mAnattribs[labelOffset].Symbol.
///
/// Offset with the just-renamed label.
/// Previous value.
private void RefactorLabel(int labelOffset, string oldLabel) {
if (!mXrefs.TryGetValue(labelOffset, out XrefSet xrefs)) {
// This can happen if you add a label in a file section that nothing references,
// and then rename it.
Debug.WriteLine("RefactorLabel: no references to " + oldLabel);
return;
}
string newLabel = mAnattribs[labelOffset].Symbol.Label;
//
// Update format descriptors in Anattribs.
//
foreach (XrefSet.Xref xr in xrefs) {
FormatDescriptor dfd = mAnattribs[xr.Offset].DataDescriptor;
if (dfd == null) {
// Should be a data target reference here? Where'd the xref come from?
Debug.Assert(false);
continue;
}
if (!dfd.HasSymbol) {
// The auto-gen stuff would have created a symbol, but the user can
// override that and display as e.g. hex.
continue;
}
if (!Label.LABEL_COMPARER.Equals(oldLabel, dfd.SymbolRef.Label)) {
// This can happen if the xref is based on the operand offset,
// but the user picked a different symbol. The xref generator
// creates entries for both target offsets, but only one will
// have a matching label.
continue;
}
mAnattribs[xr.Offset].DataDescriptor = FormatDescriptor.Create(
dfd.Length, new WeakSymbolRef(newLabel, dfd.SymbolRef.ValuePart),
dfd.FormatType == FormatDescriptor.Type.NumericBE);
}
//
// Update value in OperandFormats.
//
foreach (XrefSet.Xref xr in xrefs) {
if (!OperandFormats.TryGetValue(xr.Offset, out FormatDescriptor dfd)) {
// Probably an auto-generated symbol ref, so no entry in OperandFormats.
continue;
}
if (!dfd.HasSymbol) {
continue;
}
if (!Label.LABEL_COMPARER.Equals(oldLabel, dfd.SymbolRef.Label)) {
continue;
}
Debug.WriteLine("Replacing OpFor symbol at +" + xr.Offset.ToString("x6") +
" with " + newLabel);
OperandFormats[xr.Offset] = FormatDescriptor.Create(
dfd.Length, new WeakSymbolRef(newLabel, dfd.SymbolRef.ValuePart),
dfd.FormatType == FormatDescriptor.Type.NumericBE);
}
}
///
/// Applies the values in the set to the project hints.
///
/// Previous values; must match current contents.
/// Values to apply.
private void ApplyTypeHints(TypedRangeSet oldSet, TypedRangeSet newSet) {
CodeAnalysis.TypeHint[] hints = TypeHints;
foreach (TypedRangeSet.Tuple tuple in newSet) {
CodeAnalysis.TypeHint curType = hints[tuple.Value];
if (!oldSet.GetType(tuple.Value, out int oldType) || oldType != (int)curType) {
Debug.WriteLine("Type mismatch at " + tuple.Value);
Debug.Assert(false);
}
//Debug.WriteLine("Set +" + tuple.Value.ToString("x6") + " to " +
// (CodeAnalysis.TypeHint)tuple.Type + " (was " +
// curType + ")");
hints[tuple.Value] = (CodeAnalysis.TypeHint)tuple.Type;
}
}
#endregion Change Management
///
/// Finds a label by name. SymbolTable must be populated.
///
/// Label to find.
/// File offset associated with label, or -1 if not found.
public int FindLabelOffsetByName(string name) {
// We're interested in user labels and auto-generated labels. Do a lookup in
// SymbolTable to find the symbol, then if it's user or auto, we do a second
// search to find the file offset it's associated with. The second search
// requires a linear walk through anattribs; if we do this often we'll want to
// maintain a symbol-to-offset structure.
//
// This will not find "hidden" labels, i.e. labels that are in the middle of an
// instruction or multi-byte data area, because those are removed from SymbolTable.
if (!SymbolTable.TryGetValue(name, out Symbol sym)) {
return -1;
}
if (!sym.IsInternalLabel) {
return -1;
}
for (int i = 0; i < mAnattribs.Length; i++) {
if (mAnattribs[i].Symbol == sym) {
return i;
}
}
Debug.WriteLine("NOTE: symbol '" + name + "' exists, but wasn't found in labels");
return -1;
}
///
/// For debugging purposes, get some information about the currently loaded
/// extension scripts.
///
public string DebugGetLoadedScriptInfo() {
return mScriptManager.DebugGetLoadedScriptInfo();
}
}
}