1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-09-26 12:54:48 +00:00

Show formatted data in the code list

The disassembled lines are now shown in the custom-styled list view.
The DisplayList isn't being kept up to date on edits, but since we
can't edit anything yet that's not too limiting.

Pulled more code over, including the mostly-GUI-agnostic bits of the
source generation and assembler execution code.
This commit is contained in:
Andy McFadden 2019-05-27 18:46:09 -07:00
parent c4a056bd0a
commit 17af7efbbb
20 changed files with 4428 additions and 136 deletions

View File

@ -0,0 +1,869 @@
/*
* 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.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using Asm65;
using CommonUtil;
namespace SourceGenWPF.AsmGen {
#region IGenerator
/// <summary>
/// Generate source code compatible with the cc65 assembler (https://github.com/cc65/cc65).
/// </summary>
public class GenCc65 : IGenerator {
private const string ASM_FILE_SUFFIX = "_cc65.S"; // must start with underscore
private const string CFG_FILE_SUFFIX = "_cc65.cfg"; // ditto
private const int MAX_OPERAND_LEN = 64;
// IGenerator
public DisasmProject Project { get; private set; }
// IGenerator
public Formatter SourceFormatter { get; private set; }
// IGenerator
public AppSettings Settings { get; private set; }
// IGenerator
public AssemblerQuirks Quirks { get; private set; }
// IGenerator
public LabelLocalizer Localizer { get { return mLocalizer; } }
/// <summary>
/// Working directory, i.e. where we write our output file(s).
/// </summary>
private string mWorkDirectory;
/// <summary>
/// If set, long labels get their own line.
/// </summary>
private bool mLongLabelNewLine;
/// <summary>
/// Output column widths.
/// </summary>
private int[] mColumnWidths;
/// <summary>
/// Base filename. Typically the project file name without the ".dis65" extension.
/// </summary>
private string mFileNameBase;
/// <summary>
/// StringBuilder to use when composing a line. Held here to reduce allocations.
/// </summary>
private StringBuilder mLineBuilder = new StringBuilder(100);
/// <summary>
/// Label localization helper.
/// </summary>
private LabelLocalizer mLocalizer;
/// <summary>
/// Stream to send the output to.
/// </summary>
private StreamWriter mOutStream;
/// <summary>
/// The first time we output a high-ASCII string, we generate a macro for it.
/// </summary>
private bool mHighAsciiMacroOutput;
/// <summary>
/// Holds detected version of configured assembler.
/// </summary>
private CommonUtil.Version mAsmVersion = CommonUtil.Version.NO_VERSION;
// We test against this in a few places.
private static CommonUtil.Version V2_17 = new CommonUtil.Version(2, 17);
// Pseudo-op string constants.
private static PseudoOp.PseudoOpNames sDataOpNames = new PseudoOp.PseudoOpNames() {
EquDirective = "=",
OrgDirective = ".org",
//RegWidthDirective // .a8, .a16, .i8, .i16
DefineData1 = ".byte",
DefineData2 = ".word",
DefineData3 = ".faraddr",
DefineData4 = ".dword",
DefineBigData2 = ".dbyt",
//DefineBigData3
//DefineBigData4
Fill = ".res",
//Dense // no equivalent, use .byte with comma-separated args
StrGeneric = ".byte",
//StrReverse
StrNullTerm = ".asciiz",
//StrLen8 // macro with .strlen?
//StrLen16
//StrDci
//StrDciReverse
};
// IGenerator
public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps,
out Formatter.FormatConfig formatConfig) {
pseudoOps = new PseudoOp.PseudoOpNames() {
EquDirective = "=",
OrgDirective = ".org",
DefineData1 = ".byte",
DefineData2 = ".word",
DefineData3 = ".faraddr",
DefineData4 = ".dword",
DefineBigData2 = ".dbyt",
Fill = ".res",
StrGeneric = ".byte",
StrNullTerm = ".asciiz",
};
formatConfig = new Formatter.FormatConfig();
SetFormatConfigValues(ref formatConfig);
}
// IGenerator
public void Configure(DisasmProject project, string workDirectory, string fileNameBase,
AssemblerVersion asmVersion, AppSettings settings) {
Debug.Assert(project != null);
Debug.Assert(!string.IsNullOrEmpty(workDirectory));
Debug.Assert(!string.IsNullOrEmpty(fileNameBase));
Project = project;
Quirks = new AssemblerQuirks();
if (asmVersion != null) {
// Use the actual version. If it's > 2.17 we'll try to take advantage of
// bug fixes.
mAsmVersion = asmVersion.Version;
} else {
// No assembler installed. Use 2.17.
mAsmVersion = V2_17;
}
if (mAsmVersion <= V2_17) {
// cc65 v2.17: https://github.com/cc65/cc65/issues/717
Quirks.BlockMoveArgsReversed = true;
// cc65 v2.17: https://github.com/cc65/cc65/issues/754
Quirks.NoPcRelBankWrap = true;
}
Quirks.SinglePassAssembler = true;
mWorkDirectory = workDirectory;
mFileNameBase = fileNameBase;
Settings = settings;
mLongLabelNewLine = Settings.GetBool(AppSettings.SRCGEN_LONG_LABEL_NEW_LINE, false);
AssemblerConfig config = AssemblerConfig.GetConfig(settings,
AssemblerInfo.Id.Cc65);
mColumnWidths = (int[])config.ColumnWidths.Clone();
}
/// <summary>
/// Configures the assembler-specific format items.
/// </summary>
private void SetFormatConfigValues(ref Formatter.FormatConfig config) {
config.mForceAbsOpcodeSuffix = string.Empty;
config.mForceLongOpcodeSuffix = string.Empty;
config.mForceDirectOperandPrefix = "z:"; // zero
config.mForceAbsOperandPrefix = "a:"; // absolute
config.mForceLongOperandPrefix = "f:"; // far
config.mEndOfLineCommentDelimiter = ";";
config.mFullLineCommentDelimiterBase = ";";
config.mBoxLineCommentDelimiter = ";";
config.mAllowHighAsciiCharConst = false;
config.mExpressionMode = Formatter.FormatConfig.ExpressionMode.Cc65;
}
// IGenerator
public List<string> GenerateSource(BackgroundWorker worker) {
List<string> pathNames = new List<string>(1);
string pathName = Path.Combine(mWorkDirectory, mFileNameBase + ASM_FILE_SUFFIX);
pathNames.Add(pathName);
string cfgName = Path.Combine(mWorkDirectory, mFileNameBase + CFG_FILE_SUFFIX);
pathNames.Add(cfgName);
Formatter.FormatConfig config = new Formatter.FormatConfig();
GenCommon.ConfigureFormatterFromSettings(Settings, ref config);
SetFormatConfigValues(ref config);
SourceFormatter = new Formatter(config);
string msg = string.Format(Res.Strings.PROGRESS_GENERATING_FMT, pathName);
worker.ReportProgress(0, msg);
mLocalizer = new LabelLocalizer(Project);
if (!Settings.GetBool(AppSettings.SRCGEN_DISABLE_LABEL_LOCALIZATION, false)) {
mLocalizer.LocalPrefix = "@";
mLocalizer.Analyze();
}
// Use UTF-8 encoding, without a byte-order mark.
using (StreamWriter sw = new StreamWriter(cfgName, false, new UTF8Encoding(false))) {
GenerateLinkerScript(sw);
}
using (StreamWriter sw = new StreamWriter(pathName, false, new UTF8Encoding(false))) {
mOutStream = sw;
if (Settings.GetBool(AppSettings.SRCGEN_ADD_IDENT_COMMENT, false)) {
//if (mAsmVersion.IsValid && mAsmVersion <= V2_17) {
// OutputLine(SourceFormatter.FullLineCommentDelimiter +
// string.Format(Properties.Resources.GENERATED_FOR_VERSION,
// "cc65", mAsmVersion.ToString()));
//} else {
// OutputLine(SourceFormatter.FullLineCommentDelimiter +
// string.Format(Properties.Resources.GENERATED_FOR_LATEST, "cc65"));
//}
// Currently generating code for v2.17.
OutputLine(SourceFormatter.FullLineCommentDelimiter +
string.Format(Res.Strings.GENERATED_FOR_VERSION_FMT,
"cc65", V2_17,
AsmCc65.OPTIONS + " -C " + Path.GetFileName(cfgName)));
}
GenCommon.Generate(this, sw, worker);
}
mOutStream = null;
return pathNames;
}
private void GenerateLinkerScript(StreamWriter sw) {
sw.WriteLine("# 6502bench SourceGen generated linker script for " + mFileNameBase);
sw.WriteLine("MEMORY {");
sw.WriteLine(" MAIN: file=%O, start=%S, size=65536;");
for (int i = 0; i < Project.AddrMap.Count; i++) {
AddressMap.AddressMapEntry ame = Project.AddrMap[i];
sw.WriteLine(string.Format("# MEM{0:D3}: file=%O, start=${1:x4}, size={2};",
i, ame.Addr, ame.Length));
}
sw.WriteLine("}");
sw.WriteLine("SEGMENTS {");
sw.WriteLine(" CODE: load=MAIN, type=rw;");
for (int i = 0; i < Project.AddrMap.Count; i++) {
sw.WriteLine(string.Format("# SEG{0:D3}: load=MEM{0:D3}, type=rw;", i));
}
sw.WriteLine("}");
sw.WriteLine("FEATURES {}");
sw.WriteLine("SYMBOLS {}");
}
// IGenerator
public void OutputAsmConfig() {
CpuDef cpuDef = Project.CpuDef;
string cpuStr;
if (cpuDef.Type == CpuDef.CpuType.Cpu65816) {
cpuStr = "65816";
} else if (cpuDef.Type == CpuDef.CpuType.Cpu65C02) {
cpuStr = "65C02";
} else if (cpuDef.Type == CpuDef.CpuType.Cpu6502 && cpuDef.HasUndocumented) {
cpuStr = "6502X";
} else {
cpuStr = "6502";
}
OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(".setcpu"),
'\"' + cpuStr + '\"', string.Empty);
}
/// <summary>
/// Map the undocumented opcodes to the cc65 mnemonics. There's almost no difference
/// vs. the Unintended Opcodes mnemonics.
///
/// We don't include the double- and triple-byte NOPs here, as cc65 doesn't
/// appear to have a definition for them (as of 2.17). We also omit the alias
/// for SBC. These will all be output as hex.
/// </summary>
private static Dictionary<string, string> sUndocMap = new Dictionary<string, string>() {
{ OpName.ALR, "alr" }, // imm 0x4b
{ OpName.ANC, "anc" }, // imm 0x0b (and others)
{ OpName.ANE, "ane" }, // imm 0x8b
{ OpName.ARR, "arr" }, // imm 0x6b
{ OpName.DCP, "dcp" }, // abs 0xcf
{ OpName.ISC, "isc" }, // abs 0xef
{ OpName.JAM, "jam" }, // abs 0x02 (and others)
{ OpName.LAS, "las" }, // abs,y 0xbb
{ OpName.LAX, "lax" }, // imm 0xab; abs 0xaf
{ OpName.RLA, "rla" }, // abs 0x2f
{ OpName.RRA, "rra" }, // abs 0x6f
{ OpName.SAX, "sax" }, // abs 0x8f
{ OpName.SBX, "axs" }, //* imm 0xcb
{ OpName.SHA, "sha" }, // abs,y 0x9f
{ OpName.SHX, "shx" }, // abs,y 0x9e
{ OpName.SHY, "shy" }, // abs,x 0x9c
{ OpName.SLO, "slo" }, // abs 0x0f
{ OpName.SRE, "sre" }, // abs 0x4f
{ OpName.TAS, "tas" }, // abs,y 0x9b
};
// IGenerator
public string ModifyOpcode(int offset, OpDef op) {
if ((op == OpDef.OpWDM_WDM || op == OpDef.OpBRK_StackInt) && mAsmVersion <= V2_17) {
// cc65 v2.17 doesn't support WDM, and assembles BRK <arg> to opcode $05.
// https://github.com/cc65/cc65/issues/715
// https://github.com/cc65/cc65/issues/716
return null;
} else if (op.IsUndocumented) {
if (sUndocMap.TryGetValue(op.Mnemonic, out string newValue)) {
if ((op.Mnemonic == OpName.ANC && op.Opcode != 0x0b) ||
(op.Mnemonic == OpName.JAM && op.Opcode != 0x02)) {
// There are multiple opcodes for the same thing. cc65 outputs
// one specific thing, so we need to match that, and just do a hex
// dump for the others.
return null;
}
return newValue;
}
// Unmapped values include DOP, TOP, and the alternate SBC. Output hex.
return null;
} else {
return string.Empty;
}
}
// IGenerator
public void GenerateShortSequence(int offset, int length, out string opcode,
out string operand) {
Debug.Assert(length >= 1 && length <= 4);
// Use a comma-separated list of individual hex bytes.
opcode = sDataOpNames.DefineData1;
StringBuilder sb = new StringBuilder(length * 4);
for (int i = 0; i < length; i++) {
if (i != 0) {
sb.Append(',');
}
sb.Append(SourceFormatter.FormatHexValue(Project.FileData[offset + i], 2));
}
operand = sb.ToString();
}
// IGenerator
public void OutputDataOp(int offset) {
Formatter formatter = SourceFormatter;
byte[] data = Project.FileData;
Anattrib attr = Project.GetAnattrib(offset);
string labelStr = string.Empty;
if (attr.Symbol != null) {
labelStr = mLocalizer.ConvLabel(attr.Symbol.Label);
}
string commentStr = SourceFormatter.FormatEolComment(Project.Comments[offset]);
string opcodeStr, operandStr;
FormatDescriptor dfd = attr.DataDescriptor;
Debug.Assert(dfd != null);
int length = dfd.Length;
Debug.Assert(length > 0);
bool multiLine = false;
switch (dfd.FormatType) {
case FormatDescriptor.Type.Default:
if (length != 1) {
Debug.Assert(false);
length = 1;
}
opcodeStr = sDataOpNames.DefineData1;
int operand = RawData.GetWord(data, offset, length, false);
operandStr = formatter.FormatHexValue(operand, length * 2);
break;
case FormatDescriptor.Type.NumericLE:
opcodeStr = sDataOpNames.GetDefineData(length);
operand = RawData.GetWord(data, offset, length, false);
operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable,
mLocalizer.LabelMap, dfd, operand, length,
PseudoOp.FormatNumericOpFlags.None);
break;
case FormatDescriptor.Type.NumericBE:
opcodeStr = sDataOpNames.GetDefineBigData(length);
if (opcodeStr == null) {
// Nothing defined, output as comma-separated single-byte values.
GenerateShortSequence(offset, length, out opcodeStr, out operandStr);
} else {
operand = RawData.GetWord(data, offset, length, true);
operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable,
mLocalizer.LabelMap, dfd, operand, length,
PseudoOp.FormatNumericOpFlags.None);
}
break;
case FormatDescriptor.Type.Fill:
opcodeStr = sDataOpNames.Fill;
operandStr = length + "," + formatter.FormatHexValue(data[offset], 2);
break;
case FormatDescriptor.Type.Dense:
multiLine = true;
opcodeStr = operandStr = null;
OutputDenseHex(offset, length, labelStr, commentStr);
break;
case FormatDescriptor.Type.String:
multiLine = true;
opcodeStr = operandStr = null;
OutputString(offset, labelStr, commentStr);
break;
default:
opcodeStr = "???";
operandStr = "***";
break;
}
if (!multiLine) {
opcodeStr = formatter.FormatPseudoOp(opcodeStr);
OutputLine(labelStr, opcodeStr, operandStr, commentStr);
}
}
private void OutputDenseHex(int offset, int length, string labelStr, string commentStr) {
Formatter formatter = SourceFormatter;
byte[] data = Project.FileData;
StringBuilder sb = new StringBuilder(MAX_OPERAND_LEN);
string opcodeStr = formatter.FormatPseudoOp(sDataOpNames.DefineData1);
int maxPerLine = MAX_OPERAND_LEN / 4;
int numChunks = (length + maxPerLine - 1) / maxPerLine;
for (int chunk = 0; chunk < numChunks; chunk++) {
int chunkStart = chunk * maxPerLine;
int chunkEnd = Math.Min((chunk + 1) * maxPerLine, length);
for (int i = chunkStart; i < chunkEnd; i++) {
if (i != chunkStart) {
sb.Append(',');
}
sb.Append(formatter.FormatHexValue(data[offset + i], 2));
}
OutputLine(labelStr, opcodeStr, sb.ToString(), commentStr);
labelStr = commentStr = string.Empty;
sb.Clear();
}
}
/// <summary>
/// Outputs formatted data in an unformatted way, because the code generator couldn't
/// figure out how to do something better.
/// </summary>
private void OutputNoJoy(int offset, int length, string labelStr, string commentStr) {
byte[] data = Project.FileData;
Debug.Assert(length > 0);
Debug.Assert(offset >= 0 && offset < data.Length);
bool singleValue = true;
byte val = data[offset];
for (int i = 1; i < length; i++) {
if (data[offset + i] != val) {
singleValue = false;
break;
}
}
if (singleValue) {
string opcodeStr = SourceFormatter.FormatPseudoOp(sDataOpNames.Fill);
string operandStr = length + "," + SourceFormatter.FormatHexValue(val, 2);
OutputLine(labelStr, opcodeStr, operandStr, commentStr);
} else {
OutputDenseHex(offset, length, labelStr, commentStr);
}
}
// IGenerator
public void OutputEquDirective(string name, string valueStr, string comment) {
OutputLine(name, SourceFormatter.FormatPseudoOp(sDataOpNames.EquDirective),
valueStr, SourceFormatter.FormatEolComment(comment));
}
// IGenerator
public void OutputOrgDirective(int offset, int address) {
// Linear search for offset. List should be small, so this should be quick.
int index = 0;
foreach (AddressMap.AddressMapEntry ame in Project.AddrMap) {
if (ame.Offset == offset) {
break;
}
index++;
}
mLineBuilder.Clear();
TextUtil.AppendPaddedString(mLineBuilder, ";", mColumnWidths[0]);
TextUtil.AppendPaddedString(mLineBuilder, SourceFormatter.FormatPseudoOp(" .segment"),
mColumnWidths[1]);
mLineBuilder.AppendFormat("\"SEG{0:D3}\"", index);
OutputLine(mLineBuilder.ToString());
OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(sDataOpNames.OrgDirective),
SourceFormatter.FormatHexValue(address, 4), string.Empty);
}
// IGenerator
public void OutputRegWidthDirective(int offset, int prevM, int prevX, int newM, int newX) {
if (prevM != newM) {
string mop = (newM == 0) ? ".a16" : ".a8";
OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(mop),
string.Empty, string.Empty);
}
if (prevX != newX) {
string xop = (newX == 0) ? ".i16" : ".i8";
OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(xop),
string.Empty, string.Empty);
}
}
// IGenerator
public void OutputLine(string fullLine) {
mOutStream.WriteLine(fullLine);
}
// IGenerator
public void OutputLine(string label, string opcode, string operand, string comment) {
// If a label is provided, and it doesn't start with a '.' (indicating that it's
// a directive), and this isn't an EQU directive, add a ':'. Might be easier to
// just ".feature labels_without_colons", but I'm trying to do things the way
// that cc65 users will expect.
if (!string.IsNullOrEmpty(label) && label[0] != '.' &&
!string.Equals(opcode, sDataOpNames.EquDirective,
StringComparison.InvariantCultureIgnoreCase)) {
label += ':';
if (mLongLabelNewLine && label.Length >= mColumnWidths[0]) {
mOutStream.WriteLine(label);
label = string.Empty;
}
}
mLineBuilder.Clear();
TextUtil.AppendPaddedString(mLineBuilder, label, mColumnWidths[0]);
TextUtil.AppendPaddedString(mLineBuilder, opcode, mColumnWidths[0] + mColumnWidths[1]);
TextUtil.AppendPaddedString(mLineBuilder, operand,
mColumnWidths[0] + mColumnWidths[1] + mColumnWidths[2]);
if (string.IsNullOrEmpty(comment)) {
// Trim trailing spaces off of opcode or operand. If they want trailing
// spaces at the end of a comment, that's fine.
CommonUtil.TextUtil.TrimEnd(mLineBuilder);
} else {
mLineBuilder.Append(comment);
}
mOutStream.WriteLine(mLineBuilder.ToString());
}
private void OutputString(int offset, string labelStr, string commentStr) {
// Normal ASCII strings are straightforward: they're just part of a .byte
// directive, and can mix with anything else in the .byte.
//
// For CString we can use .asciiz, but only if the string fits on one line
// and doesn't include delimiters. For L8String and L16String we can
// define simple macros, but their use has a similar restriction. High-ASCII
// strings also require a macro.
//
// We might be able to define a macro for DCI and Reverse as well.
//
// The limitation on strings with delimiters arises because (1) I don't see a
// way to escape them within a string, and (2) the simple macro workarounds
// only take a single argument, not a comma-separated list of stuff.
//
// Some ideas here:
// https://groups.google.com/forum/#!topic/comp.sys.apple2.programmer/5Wkw8mUPcU0
Formatter formatter = SourceFormatter;
byte[] data = Project.FileData;
Anattrib attr = Project.GetAnattrib(offset);
FormatDescriptor dfd = attr.DataDescriptor;
Debug.Assert(dfd != null);
Debug.Assert(dfd.FormatType == FormatDescriptor.Type.String);
Debug.Assert(dfd.Length > 0);
bool highAscii = false;
int leadingBytes = 0;
int trailingBytes = 0;
bool showLeading = false;
bool showTrailing = false;
switch (dfd.FormatSubType) {
case FormatDescriptor.SubType.None:
highAscii = (data[offset] & 0x80) != 0;
break;
case FormatDescriptor.SubType.Dci:
highAscii = (data[offset] & 0x80) != 0;
trailingBytes = 1;
showTrailing = true;
break;
case FormatDescriptor.SubType.Reverse:
highAscii = (data[offset] & 0x80) != 0;
break;
case FormatDescriptor.SubType.DciReverse:
highAscii = (data[offset + dfd.Length - 1] & 0x80) != 0;
leadingBytes = 1;
showLeading = true;
break;
case FormatDescriptor.SubType.CString:
highAscii = (data[offset] & 0x80) != 0;
trailingBytes = 1;
showTrailing = true;
break;
case FormatDescriptor.SubType.L8String:
if (dfd.Length > 1) {
highAscii = (data[offset + 1] & 0x80) != 0;
}
leadingBytes = 1;
showLeading = true;
break;
case FormatDescriptor.SubType.L16String:
if (dfd.Length > 2) {
highAscii = (data[offset + 2] & 0x80) != 0;
}
leadingBytes = 2;
showLeading = true;
break;
default:
Debug.Assert(false);
return;
}
char delim = '"';
StringGather gath = null;
// Run the string through so we can see if it'll fit on one line. As a minor
// optimization, we skip this step for "generic" strings, which are probably
// the most common thing.
if (dfd.FormatSubType != FormatDescriptor.SubType.None || highAscii) {
gath = new StringGather(this, labelStr, "???", commentStr, delim,
delim, StringGather.ByteStyle.CommaSep, MAX_OPERAND_LEN, true);
FeedGath(gath, data, offset, dfd.Length, leadingBytes, showLeading,
trailingBytes, showTrailing);
Debug.Assert(gath.NumLinesOutput > 0);
}
string opcodeStr = formatter.FormatPseudoOp(sDataOpNames.StrGeneric);
switch (dfd.FormatSubType) {
case FormatDescriptor.SubType.None:
// Special case for simple short high-ASCII strings. These have no
// leading or trailing bytes. We can improve this a bit by handling
// arbitrarily long strings by simply breaking them across lines.
Debug.Assert(leadingBytes == 0);
Debug.Assert(trailingBytes == 0);
if (highAscii && gath.NumLinesOutput == 1 && !gath.HasDelimiter) {
if (!mHighAsciiMacroOutput) {
mHighAsciiMacroOutput = true;
// Output a macro for high-ASCII strings.
OutputLine(".macro", "HiAscii", "Arg", string.Empty);
OutputLine(string.Empty, ".repeat", ".strlen(Arg), I", string.Empty);
OutputLine(string.Empty, ".byte", ".strat(Arg, I) | $80", string.Empty);
OutputLine(string.Empty, ".endrep", string.Empty, string.Empty);
OutputLine(".endmacro", string.Empty, string.Empty, string.Empty);
}
opcodeStr = formatter.FormatPseudoOp("HiAscii");
highAscii = false;
}
break;
case FormatDescriptor.SubType.Dci:
case FormatDescriptor.SubType.Reverse:
case FormatDescriptor.SubType.DciReverse:
// Full configured above.
break;
case FormatDescriptor.SubType.CString:
if (gath.NumLinesOutput == 1 && !gath.HasDelimiter) {
opcodeStr = sDataOpNames.StrNullTerm;
showTrailing = false;
}
break;
case FormatDescriptor.SubType.L8String:
case FormatDescriptor.SubType.L16String:
// Implement macros?
break;
default:
Debug.Assert(false);
return;
}
if (highAscii) {
OutputNoJoy(offset, dfd.Length, labelStr, commentStr);
return;
}
// Create a new StringGather, with the final opcode choice.
gath = new StringGather(this, labelStr, opcodeStr, commentStr, delim,
delim, StringGather.ByteStyle.CommaSep, MAX_OPERAND_LEN, false);
FeedGath(gath, data, offset, dfd.Length, leadingBytes, showLeading,
trailingBytes, showTrailing);
}
/// <summary>
/// Feeds the bytes into the StringGather.
/// </summary>
private void FeedGath(StringGather gath, byte[] data, int offset, int length,
int leadingBytes, bool showLeading, int trailingBytes, bool showTrailing) {
int startOffset = offset;
int strEndOffset = offset + length - trailingBytes;
if (showLeading) {
while (leadingBytes-- > 0) {
gath.WriteByte(data[offset++]);
}
} else {
offset += leadingBytes;
}
for (; offset < strEndOffset; offset++) {
gath.WriteChar((char)(data[offset] & 0x7f));
}
while (showTrailing && trailingBytes-- > 0) {
gath.WriteByte(data[offset++]);
}
gath.Finish();
}
}
#endregion IGenerator
#region IAssembler
/// <summary>
/// Cross-assembler execution interface.
/// </summary>
public class AsmCc65 : IAssembler {
// Fixed options. "--target none" is needed to neutralize the character encoding,
// which seems to default to PETSCII.
public const string OPTIONS = "--target none";
// Paths from generator.
private List<string> mPathNames;
// Directory to make current before executing assembler.
private string mWorkDirectory;
// IAssembler
public void GetExeIdentifiers(out string humanName, out string exeName) {
humanName = "cc65 CL";
exeName = "cl65";
}
// IAssembler
public AssemblerConfig GetDefaultConfig() {
return new AssemblerConfig(string.Empty, new int[] { 9, 8, 11, 72 });
}
// IAssembler
public AssemblerVersion QueryVersion() {
AssemblerConfig config =
AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Cc65);
if (config == null || string.IsNullOrEmpty(config.ExecutablePath)) {
return null;
}
ShellCommand cmd = new ShellCommand(config.ExecutablePath, "--version",
Directory.GetCurrentDirectory(), null);
cmd.Execute();
if (string.IsNullOrEmpty(cmd.Stdout)) {
return null;
}
// Windows - Stderr: "cl65.exe V2.17\r\n"
// Linux - Stderr: "cl65 V2.17 - Git N/A\n"
// Other platforms may not have the ".exe". Find first occurrence of " V".
const string PREFIX = " V";
string str = cmd.Stderr;
int start = str.IndexOf(PREFIX);
int end = (start < 0) ? -1 : str.IndexOfAny(new char[] { ' ', '\r', '\n' }, start + 1);
if (start < 0 || end < 0 || start + PREFIX.Length >= end) {
Debug.WriteLine("Couldn't find version in " + str);
return null;
}
start += PREFIX.Length;
string versionStr = str.Substring(start, end - start);
CommonUtil.Version version = CommonUtil.Version.Parse(versionStr);
if (!version.IsValid) {
return null;
}
return new AssemblerVersion(versionStr, version);
}
// IAssembler
public void Configure(List<string> pathNames, string workDirectory) {
// Clone pathNames, in case the caller decides to modify the original.
mPathNames = new List<string>(pathNames.Count);
foreach (string str in pathNames) {
mPathNames.Add(str);
}
mWorkDirectory = workDirectory;
}
// IAssembler
public AssemblerResults RunAssembler(BackgroundWorker worker) {
Debug.Assert(mPathNames.Count == 2);
string pathName = StripWorkDirectory(mPathNames[0]);
string cfgName = StripWorkDirectory(mPathNames[1]);
AssemblerConfig config =
AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Cc65);
if (string.IsNullOrEmpty(config.ExecutablePath)) {
Debug.WriteLine("Assembler not configured");
return null;
}
string cfgOpt = " -C \"" + cfgName + "\"";
worker.ReportProgress(0, Res.Strings.PROGRESS_ASSEMBLING);
// Wrap pathname in quotes in case it has spaces.
// (Do we need to shell-escape quotes in the pathName?)
ShellCommand cmd = new ShellCommand(config.ExecutablePath,
OPTIONS + cfgOpt + " \"" + pathName + "\"", mWorkDirectory, null);
cmd.Execute();
// Can't really do anything with a "cancel" request.
// Output filename is the input filename without the ".S". Since the filename
// was generated by us we can be confident in the format.
string outputFile = mPathNames[0].Substring(0, mPathNames[0].Length - 2);
return new AssemblerResults(cmd.FullCommandLine, cmd.ExitCode, cmd.Stdout,
cmd.Stderr, outputFile);
}
/// <summary>
/// Reduce input file to a partial path if possible. This is just to make
/// what we display to the user a little easier to read.
/// </summary>
/// <param name="pathName">Full pathname of file.</param>
/// <returns>Pathname with working directory prefix stripped off.</returns>
private string StripWorkDirectory(string pathName) {
if (pathName.StartsWith(mWorkDirectory)) {
return pathName.Remove(0, mWorkDirectory.Length + 1);
} else {
// Unexpected, but shouldn't be a problem.
Debug.WriteLine("NOTE: source file is not in work directory");
return pathName;
}
}
}
#endregion IAssembler
}

View File

@ -0,0 +1,790 @@
/*
* 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.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using Asm65;
using CommonUtil;
namespace SourceGenWPF.AsmGen {
#region IGenerator
/// <summary>
/// Generate source code compatible with Brutal Deluxe's Merlin 32 assembler
/// (https://www.brutaldeluxe.fr/products/crossdevtools/merlin/).
/// </summary>
public class GenMerlin32 : IGenerator {
private const string ASM_FILE_SUFFIX = "_Merlin32.S"; // must start with underscore
private const int MAX_OPERAND_LEN = 64;
// IGenerator
public DisasmProject Project { get; private set; }
// IGenerator
public Formatter SourceFormatter { get; private set; }
// IGenerator
public AppSettings Settings { get; private set; }
// IGenerator
public AssemblerQuirks Quirks { get; private set; }
// IGenerator
public LabelLocalizer Localizer { get { return mLocalizer; } }
/// <summary>
/// Working directory, i.e. where we write our output file(s).
/// </summary>
private string mWorkDirectory;
/// <summary>
/// If set, long labels get their own line.
/// </summary>
private bool mLongLabelNewLine;
/// <summary>
/// Output column widths.
/// </summary>
private int[] mColumnWidths;
/// <summary>
/// Base filename. Typically the project file name without the ".dis65" extension.
/// </summary>
private string mFileNameBase;
/// <summary>
/// StringBuilder to use when composing a line. Held here to reduce allocations.
/// </summary>
private StringBuilder mLineBuilder = new StringBuilder(100);
/// <summary>
/// Label localization helper.
/// </summary>
private LabelLocalizer mLocalizer;
/// <summary>
/// Stream to send the output to.
/// </summary>
private StreamWriter mOutStream;
/// <summary>
/// Holds detected version of configured assembler.
/// </summary>
private CommonUtil.Version mAsmVersion = CommonUtil.Version.NO_VERSION;
// Semi-convenient way to hold all the interesting string constants in one place.
// Note the actual usage of the pseudo-op may not match what the main app does,
// e.g. RegWidthDirective behaves differently from "mx". I'm just trying to avoid
// having string constants scattered all over.
private static PseudoOp.PseudoOpNames sDataOpNames = new PseudoOp.PseudoOpNames() {
EquDirective = "equ",
OrgDirective = "org",
RegWidthDirective = "mx",
DefineData1 = "dfb",
DefineData2 = "dw",
DefineData3 = "adr",
DefineData4 = "adrl",
DefineBigData2 = "ddb",
//DefineBigData3
//DefineBigData4
Fill = "ds",
Dense = "hex",
StrGeneric = "asc",
StrGenericHi = "asc",
StrReverse = "rev",
StrReverseHi = "rev",
//StrNullTerm
StrLen8 = "str",
StrLen8Hi = "str",
StrLen16 = "strl",
StrLen16Hi = "strl",
StrDci = "dci",
StrDciHi = "dci",
//StrDciReverse
};
// IGenerator
public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps,
out Formatter.FormatConfig formatConfig) {
// This is not intended to match up with the Merlin generator, which uses
// the same pseudo-op for low/high ASCII but different string delimiters. We
// don't change the delimiters for the display list, so instead we tweak the
// opcode slightly.
char hiAscii = '\u2191';
pseudoOps = new PseudoOp.PseudoOpNames() {
EquDirective = "equ",
OrgDirective = "org",
DefineData1 = "dfb",
DefineData2 = "dw",
DefineData3 = "adr",
DefineData4 = "adrl",
DefineBigData2 = "ddb",
Fill = "ds",
Dense = "hex",
StrGeneric = "asc",
StrGenericHi = "asc" + hiAscii,
StrReverse = "rev",
StrReverseHi = "rev" + hiAscii,
StrLen8 = "str",
StrLen8Hi = "str" + hiAscii,
StrLen16 = "strl",
StrLen16Hi = "strl" + hiAscii,
StrDci = "dci",
StrDciHi = "dci" + hiAscii,
};
formatConfig = new Formatter.FormatConfig();
SetFormatConfigValues(ref formatConfig);
}
// IGenerator
public void Configure(DisasmProject project, string workDirectory, string fileNameBase,
AssemblerVersion asmVersion, AppSettings settings) {
Debug.Assert(project != null);
Debug.Assert(!string.IsNullOrEmpty(workDirectory));
Debug.Assert(!string.IsNullOrEmpty(fileNameBase));
Project = project;
Quirks = new AssemblerQuirks();
Quirks.TracksSepRepNotEmu = true;
Quirks.NoPcRelBankWrap = true;
mWorkDirectory = workDirectory;
mFileNameBase = fileNameBase;
Settings = settings;
mLongLabelNewLine = Settings.GetBool(AppSettings.SRCGEN_LONG_LABEL_NEW_LINE, false);
AssemblerConfig config = AssemblerConfig.GetConfig(settings,
AssemblerInfo.Id.Merlin32);
mColumnWidths = (int[])config.ColumnWidths.Clone();
}
/// <summary>
/// Configures the assembler-specific format items.
/// </summary>
private void SetFormatConfigValues(ref Formatter.FormatConfig config) {
config.mForceAbsOpcodeSuffix = ":";
config.mForceLongOpcodeSuffix = "l";
config.mForceDirectOperandPrefix = string.Empty;
config.mForceAbsOperandPrefix = string.Empty;
config.mForceLongOperandPrefix = string.Empty;
config.mEndOfLineCommentDelimiter = ";";
config.mFullLineCommentDelimiterBase = ";";
config.mBoxLineCommentDelimiter = string.Empty;
config.mAllowHighAsciiCharConst = true;
config.mExpressionMode = Formatter.FormatConfig.ExpressionMode.Merlin;
}
// IGenerator; executes on background thread
public List<string> GenerateSource(BackgroundWorker worker) {
List<string> pathNames = new List<string>(1);
string fileName = mFileNameBase + ASM_FILE_SUFFIX;
string pathName = Path.Combine(mWorkDirectory, fileName);
pathNames.Add(pathName);
Formatter.FormatConfig config = new Formatter.FormatConfig();
GenCommon.ConfigureFormatterFromSettings(Settings, ref config);
SetFormatConfigValues(ref config);
SourceFormatter = new Formatter(config);
string msg = string.Format(Res.Strings.PROGRESS_GENERATING_FMT, pathName);
worker.ReportProgress(0, msg);
mLocalizer = new LabelLocalizer(Project);
if (!Settings.GetBool(AppSettings.SRCGEN_DISABLE_LABEL_LOCALIZATION, false)) {
mLocalizer.LocalPrefix = ":";
mLocalizer.Analyze();
}
// Use UTF-8 encoding, without a byte-order mark.
using (StreamWriter sw = new StreamWriter(pathName, false, new UTF8Encoding(false))) {
mOutStream = sw;
if (Settings.GetBool(AppSettings.SRCGEN_ADD_IDENT_COMMENT, false)) {
// No version-specific stuff yet. We're generating code for v1.0.
OutputLine(SourceFormatter.FullLineCommentDelimiter +
string.Format(Res.Strings.GENERATED_FOR_VERSION_FMT,
"Merlin 32", new CommonUtil.Version(1, 0), string.Empty));
}
GenCommon.Generate(this, sw, worker);
}
mOutStream = null;
return pathNames;
}
// IGenerator
public void OutputDataOp(int offset) {
Formatter formatter = SourceFormatter;
byte[] data = Project.FileData;
Anattrib attr = Project.GetAnattrib(offset);
string labelStr = string.Empty;
if (attr.Symbol != null) {
labelStr = mLocalizer.ConvLabel(attr.Symbol.Label);
}
string commentStr = SourceFormatter.FormatEolComment(Project.Comments[offset]);
string opcodeStr, operandStr;
FormatDescriptor dfd = attr.DataDescriptor;
Debug.Assert(dfd != null);
int length = dfd.Length;
Debug.Assert(length > 0);
bool multiLine = false;
switch (dfd.FormatType) {
case FormatDescriptor.Type.Default:
if (length != 1) {
Debug.Assert(false);
length = 1;
}
opcodeStr = sDataOpNames.DefineData1;
int operand = RawData.GetWord(data, offset, length, false);
operandStr = formatter.FormatHexValue(operand, length * 2);
break;
case FormatDescriptor.Type.NumericLE:
opcodeStr = sDataOpNames.GetDefineData(length);
operand = RawData.GetWord(data, offset, length, false);
operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable,
mLocalizer.LabelMap, dfd, operand, length,
PseudoOp.FormatNumericOpFlags.None);
break;
case FormatDescriptor.Type.NumericBE:
opcodeStr = sDataOpNames.GetDefineBigData(length);
if (opcodeStr == null) {
// Nothing defined, output as comma-separated single-byte values.
GenerateShortSequence(offset, length, out opcodeStr, out operandStr);
} else {
operand = RawData.GetWord(data, offset, length, true);
operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable,
mLocalizer.LabelMap, dfd, operand, length,
PseudoOp.FormatNumericOpFlags.None);
}
break;
case FormatDescriptor.Type.Fill:
opcodeStr = sDataOpNames.Fill;
operandStr = length + "," + formatter.FormatHexValue(data[offset], 2);
break;
case FormatDescriptor.Type.Dense:
multiLine = true;
opcodeStr = operandStr = null;
OutputDenseHex(offset, length, labelStr, commentStr);
break;
case FormatDescriptor.Type.String:
multiLine = true;
opcodeStr = operandStr = null;
OutputString(offset, labelStr, commentStr);
break;
default:
opcodeStr = "???";
operandStr = "***";
break;
}
if (!multiLine) {
opcodeStr = formatter.FormatPseudoOp(opcodeStr);
OutputLine(labelStr, opcodeStr, operandStr, commentStr);
}
}
private void OutputDenseHex(int offset, int length, string labelStr, string commentStr) {
Formatter formatter = SourceFormatter;
byte[] data = Project.FileData;
int maxPerLine = MAX_OPERAND_LEN / 2;
string opcodeStr = formatter.FormatPseudoOp(sDataOpNames.Dense);
for (int i = 0; i < length; i += maxPerLine) {
int subLen = length - i;
if (subLen > maxPerLine) {
subLen = maxPerLine;
}
string operandStr = formatter.FormatDenseHex(data, offset + i, subLen);
OutputLine(labelStr, opcodeStr, operandStr, commentStr);
labelStr = commentStr = string.Empty;
}
}
// IGenerator
public string ModifyOpcode(int offset, OpDef op) {
if (op.IsUndocumented) {
return null;
}
// The assembler works correctly if the symbol is defined as a two-digit hex
// value (e.g. "foo equ $80") but fails if it's four (e.g. "foo equ $0080"). We
// output symbols with minimal digits, but this doesn't help if the code itself
// lives on zero page. If the operand is a reference to a zero-page user label,
// we need to output the instruction as hex.
// More info: https://github.com/apple2accumulator/merlin32/issues/8
if (op == OpDef.OpPEI_StackDPInd ||
op == OpDef.OpSTY_DPIndexX ||
op == OpDef.OpSTX_DPIndexY ||
op.AddrMode == OpDef.AddressMode.DPIndLong ||
op.AddrMode == OpDef.AddressMode.DPInd ||
op.AddrMode == OpDef.AddressMode.DPIndexXInd) {
FormatDescriptor dfd = Project.GetAnattrib(offset).DataDescriptor;
if (dfd != null && dfd.HasSymbol) {
// It has a symbol. See if the symbol target is a label (auto or user).
if (Project.SymbolTable.TryGetValue(dfd.SymbolRef.Label, out Symbol sym)) {
if (sym.IsInternalLabel) {
return null;
}
}
}
}
return string.Empty;
}
// IGenerator
public void GenerateShortSequence(int offset, int length, out string opcode,
out string operand) {
Debug.Assert(length >= 1 && length <= 4);
// Use a comma-separated list of individual hex bytes.
opcode = sDataOpNames.DefineData1;
StringBuilder sb = new StringBuilder(length * 4);
for (int i = 0; i < length; i++) {
if (i != 0) {
sb.Append(',');
}
sb.Append(SourceFormatter.FormatHexValue(Project.FileData[offset + i], 2));
}
operand = sb.ToString();
}
// IGenerator
public void OutputAsmConfig() {
// nothing to do
}
// IGenerator
public void OutputEquDirective(string name, string valueStr, string comment) {
OutputLine(name, SourceFormatter.FormatPseudoOp(sDataOpNames.EquDirective),
valueStr, SourceFormatter.FormatEolComment(comment));
}
// IGenerator
public void OutputOrgDirective(int offset, int address) {
OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(sDataOpNames.OrgDirective),
SourceFormatter.FormatHexValue(address, 4), string.Empty);
}
// IGenerator
public void OutputRegWidthDirective(int offset, int prevM, int prevX, int newM, int newX) {
// prevM/prevX may be ambiguous for offset 0, but otherwise everything
// should be either 0 or 1.
Debug.Assert(newM == 0 || newM == 1);
Debug.Assert(newX == 0 || newX == 1);
if (offset == 0 && newM == 1 && newX == 1) {
// Assembler defaults to short regs, so we can skip this.
return;
}
OutputLine(string.Empty,
SourceFormatter.FormatPseudoOp(sDataOpNames.RegWidthDirective),
"%" + newM + newX, string.Empty);
}
// IGenerator
public void OutputLine(string fullLine) {
mOutStream.WriteLine(fullLine);
}
// IGenerator
public void OutputLine(string label, string opcode, string operand, string comment) {
// Split long label, but not on EQU directives (confuses the assembler).
if (mLongLabelNewLine && label.Length >= mColumnWidths[0] &&
!string.Equals(opcode, sDataOpNames.EquDirective,
StringComparison.InvariantCultureIgnoreCase)) {
mOutStream.WriteLine(label);
label = string.Empty;
}
mLineBuilder.Clear();
TextUtil.AppendPaddedString(mLineBuilder, label, mColumnWidths[0]);
TextUtil.AppendPaddedString(mLineBuilder, opcode, mColumnWidths[0] + mColumnWidths[1]);
TextUtil.AppendPaddedString(mLineBuilder, operand,
mColumnWidths[0] + mColumnWidths[1] + mColumnWidths[2]);
if (string.IsNullOrEmpty(comment)) {
// Trim trailing spaces off of opcode or operand. If they want trailing
// spaces at the end of a comment, that's fine.
CommonUtil.TextUtil.TrimEnd(mLineBuilder);
} else {
mLineBuilder.Append(comment);
}
mOutStream.WriteLine(mLineBuilder.ToString());
}
private enum RevMode { Forward, Reverse, BlockReverse };
private void OutputString(int offset, string labelStr, string commentStr) {
// This gets complicated.
//
// For Dci, L8String, and L16String, the entire string needs to fit in the
// operand of one line. If it can't, we need to separate the length byte/word
// or inverted character out, and just dump the rest as ASCII. Computing the
// line length requires factoring delimiter character escapes. (NOTE: contrary
// to the documentation, STR and STRL do include trailing hex characters in the
// length calculation, so it's possible to escape delimiters.)
//
// For Reverse, we can span lines, but only if we emit the lines in
// backward order. Also, Merlin doesn't allow hex to be embedded in a REV
// operation, so we can't use REV if the string contains a delimiter.
//
// DciReverse is deprecated, but we can handle it as a Reverse string with a
// trailing byte on a following line.
//
// For aesthetic purposes, zero-length CString, L8String, and L16String
// should be output as DFB/DW zeroes rather than an empty string -- makes
// it easier to read.
Formatter formatter = SourceFormatter;
byte[] data = Project.FileData;
Anattrib attr = Project.GetAnattrib(offset);
FormatDescriptor dfd = attr.DataDescriptor;
Debug.Assert(dfd != null);
Debug.Assert(dfd.FormatType == FormatDescriptor.Type.String);
Debug.Assert(dfd.Length > 0);
bool highAscii = false;
int showZeroes = 0;
int leadingBytes = 0;
int trailingBytes = 0;
bool showLeading = false;
bool showTrailing = false;
RevMode revMode = RevMode.Forward;
switch (dfd.FormatSubType) {
case FormatDescriptor.SubType.None:
highAscii = (data[offset] & 0x80) != 0;
break;
case FormatDescriptor.SubType.Dci:
highAscii = (data[offset] & 0x80) != 0;
break;
case FormatDescriptor.SubType.Reverse:
highAscii = (data[offset] & 0x80) != 0;
revMode = RevMode.Reverse;
break;
case FormatDescriptor.SubType.DciReverse:
highAscii = (data[offset + dfd.Length - 1] & 0x80) != 0;
revMode = RevMode.Reverse;
break;
case FormatDescriptor.SubType.CString:
highAscii = (data[offset] & 0x80) != 0;
if (dfd.Length == 1) {
showZeroes = 1; // empty null-terminated string
}
trailingBytes = 1;
showTrailing = true;
break;
case FormatDescriptor.SubType.L8String:
if (dfd.Length > 1) {
highAscii = (data[offset + 1] & 0x80) != 0;
} else {
//showZeroes = 1;
}
leadingBytes = 1;
break;
case FormatDescriptor.SubType.L16String:
if (dfd.Length > 2) {
highAscii = (data[offset + 2] & 0x80) != 0;
} else {
//showZeroes = 2;
}
leadingBytes = 2;
break;
default:
Debug.Assert(false);
return;
}
if (showZeroes != 0) {
// Empty string. Just output the length byte(s) or null terminator.
GenerateShortSequence(offset, showZeroes, out string opcode, out string operand);
OutputLine(labelStr, opcode, operand, commentStr);
return;
}
// Merlin 32 uses single-quote for low ASCII, double-quote for high ASCII. When
// quoting the delimiter we use a hexadecimal value. We need to bear in mind that
// we're forcing the characters to low ASCII, but the actual character being
// escaped might be in high ASCII. Hence delim vs. delimReplace.
char delim = highAscii ? '"' : '\'';
char delimReplace = highAscii ? ((char)(delim | 0x80)) : delim;
StringGather gath = null;
// Run the string through so we can see if it'll fit on one line. As a minor
// optimization, we skip this step for "generic" strings, which are probably
// the most common thing.
if (dfd.FormatSubType != FormatDescriptor.SubType.None) {
gath = new StringGather(this, labelStr, "???", commentStr, delim,
delimReplace, StringGather.ByteStyle.DenseHex, MAX_OPERAND_LEN, true);
FeedGath(gath, data, offset, dfd.Length, revMode, leadingBytes, showLeading,
trailingBytes, showTrailing);
Debug.Assert(gath.NumLinesOutput > 0);
}
string opcodeStr;
switch (dfd.FormatSubType) {
case FormatDescriptor.SubType.None:
opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric;
break;
case FormatDescriptor.SubType.Dci:
if (gath.NumLinesOutput == 1) {
opcodeStr = highAscii ? sDataOpNames.StrDciHi : sDataOpNames.StrDci;
} else {
opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric;
trailingBytes = 1;
showTrailing = true;
}
break;
case FormatDescriptor.SubType.Reverse:
if (gath.HasDelimiter) {
// can't include escaped delimiters in REV
opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric;
revMode = RevMode.Forward;
} else if (gath.NumLinesOutput > 1) {
opcodeStr = highAscii ? sDataOpNames.StrReverseHi : sDataOpNames.StrReverse;
revMode = RevMode.BlockReverse;
} else {
opcodeStr = highAscii ? sDataOpNames.StrReverseHi : sDataOpNames.StrReverse;
Debug.Assert(revMode == RevMode.Reverse);
}
break;
case FormatDescriptor.SubType.DciReverse:
// Mostly punt -- output as ASCII with special handling for first byte.
opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric;
revMode = RevMode.Forward;
leadingBytes = 1;
showLeading = true;
break;
case FormatDescriptor.SubType.CString:
//opcodeStr = sDataOpNames.StrNullTerm[highAscii ? 1 : 0];
opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric;
break;
case FormatDescriptor.SubType.L8String:
if (gath.NumLinesOutput == 1) {
opcodeStr = highAscii ? sDataOpNames.StrLen8Hi : sDataOpNames.StrLen8;
} else {
opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric;
leadingBytes = 1;
showLeading = true;
}
break;
case FormatDescriptor.SubType.L16String:
if (gath.NumLinesOutput == 1) {
opcodeStr = highAscii ? sDataOpNames.StrLen16Hi : sDataOpNames.StrLen16;
} else {
opcodeStr = highAscii ? sDataOpNames.StrGenericHi : sDataOpNames.StrGeneric;
leadingBytes = 2;
showLeading = true;
}
break;
default:
Debug.Assert(false);
return;
}
opcodeStr = formatter.FormatPseudoOp(opcodeStr);
// Create a new StringGather, with the final opcode choice.
gath = new StringGather(this, labelStr, opcodeStr, commentStr, delim,
delimReplace, StringGather.ByteStyle.DenseHex, MAX_OPERAND_LEN, false);
FeedGath(gath, data, offset, dfd.Length, revMode, leadingBytes, showLeading,
trailingBytes, showTrailing);
}
/// <summary>
/// Feeds the bytes into the StringGather.
/// </summary>
private void FeedGath(StringGather gath, byte[] data, int offset, int length,
RevMode revMode, int leadingBytes, bool showLeading,
int trailingBytes, bool showTrailing) {
int startOffset = offset;
int strEndOffset = offset + length - trailingBytes;
if (showLeading) {
while (leadingBytes-- > 0) {
gath.WriteByte(data[offset++]);
}
} else {
offset += leadingBytes;
}
if (revMode == RevMode.BlockReverse) {
const int maxPerLine = MAX_OPERAND_LEN - 2;
int numBlockLines = (length + maxPerLine - 1) / maxPerLine;
for (int chunk = 0; chunk < numBlockLines; chunk++) {
int chunkOffset = startOffset + chunk * maxPerLine;
int endOffset = chunkOffset + maxPerLine;
if (endOffset > strEndOffset) {
endOffset = strEndOffset;
}
for (int off = endOffset - 1; off >= chunkOffset; off--) {
gath.WriteChar((char)(data[off] & 0x7f));
}
}
} else {
for (; offset < strEndOffset; offset++) {
if (revMode == RevMode.Forward) {
gath.WriteChar((char)(data[offset] & 0x7f));
} else if (revMode == RevMode.Reverse) {
int posn = startOffset + (strEndOffset - offset) - 1;
gath.WriteChar((char)(data[posn] & 0x7f));
} else {
Debug.Assert(false);
}
}
}
while (showTrailing && trailingBytes-- > 0) {
gath.WriteByte(data[offset++]);
}
gath.Finish();
}
}
#endregion IGenerator
#region IAssembler
/// <summary>
/// Cross-assembler execution interface.
/// </summary>
public class AsmMerlin32 : IAssembler {
// Paths from generator.
private List<string> mPathNames;
// Directory to make current before executing assembler.
private string mWorkDirectory;
// IAssembler
public void GetExeIdentifiers(out string humanName, out string exeName) {
humanName = "Merlin Assembler";
exeName = "Merlin32";
}
// IAssembler
public AssemblerConfig GetDefaultConfig() {
return new AssemblerConfig(string.Empty, new int[] { 9, 6, 11, 74 });
}
// IAssembler
public AssemblerVersion QueryVersion() {
AssemblerConfig config =
AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Merlin32);
if (config == null || string.IsNullOrEmpty(config.ExecutablePath)) {
return null;
}
ShellCommand cmd = new ShellCommand(config.ExecutablePath, string.Empty,
Directory.GetCurrentDirectory(), null);
cmd.Execute();
if (string.IsNullOrEmpty(cmd.Stdout)) {
return null;
}
// Stdout: "C:\Src\WorkBench\Merlin32.exe v 1.0, (c) Brutal Deluxe ..."
// Other platforms may not have the ".exe". Find first occurrence of " v ".
const string PREFIX = " v "; // not expecting this to appear in the path
string str = cmd.Stdout;
int start = str.IndexOf(PREFIX);
int end = (start < 0) ? -1 : str.IndexOf(',', start);
if (start < 0 || end < 0 || start + PREFIX.Length >= end) {
Debug.WriteLine("Couldn't find version in " + str);
return null;
}
start += PREFIX.Length;
string versionStr = str.Substring(start, end - start);
CommonUtil.Version version = CommonUtil.Version.Parse(versionStr);
if (!version.IsValid) {
return null;
}
return new AssemblerVersion(versionStr, version);
}
// IAssembler
public void Configure(List<string> pathNames, string workDirectory) {
// Clone pathNames, in case the caller decides to modify the original.
mPathNames = new List<string>(pathNames.Count);
foreach (string str in pathNames) {
mPathNames.Add(str);
}
mWorkDirectory = workDirectory;
}
// IAssembler
public AssemblerResults RunAssembler(BackgroundWorker worker) {
// Reduce input file to a partial path if possible. This is really just to make
// what we display to the user a little easier to read.
string pathName = mPathNames[0];
if (pathName.StartsWith(mWorkDirectory)) {
pathName = pathName.Remove(0, mWorkDirectory.Length + 1);
} else {
// Unexpected, but shouldn't be a problem.
Debug.WriteLine("NOTE: source file is not in work directory");
}
AssemblerConfig config =
AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Merlin32);
if (string.IsNullOrEmpty(config.ExecutablePath)) {
Debug.WriteLine("Assembler not configured");
return null;
}
worker.ReportProgress(0, Res.Strings.PROGRESS_ASSEMBLING);
// Wrap pathname in quotes in case it has spaces.
// (Do we need to shell-escape quotes in the pathName?)
//
// Merlin 32 has no options. The second argument is the macro include file path.
ShellCommand cmd = new ShellCommand(config.ExecutablePath, ". \"" + pathName + "\"",
mWorkDirectory, null);
cmd.Execute();
// Can't really do anything with a "cancel" request.
// Output filename is the input filename without the ".S". Since the filename
// was generated by us we can be confident in the format.
string outputFile = mPathNames[0].Substring(0, mPathNames[0].Length - 2);
return new AssemblerResults(cmd.FullCommandLine, cmd.ExitCode, cmd.Stdout,
cmd.Stderr, outputFile);
}
}
#endregion IAssembler
}

View File

@ -0,0 +1,778 @@
/*
* 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.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using Asm65;
using CommonUtil;
namespace SourceGenWPF.AsmGen {
#region IGenerator
/// <summary>
/// Generate source code compatible with the 64tass assembler
/// (https://sourceforge.net/projects/tass64/).
///
/// The assembler is officially called "64tass", but it's sometimes written "tass64" because
/// in some cases you can't start an identifier with a number.
///
/// We need to deal with a couple of unusual aspects:
/// (1) The prefix for a local label is '_', which is generally a legal character. So
/// if somebody creates a label with a leading '_', and it's not actually local, we have
/// to "de-local" it somehow.
/// (2) By default, labels are handled in a case-insensitive fashion, which is extremely
/// rare for programming languages. Case sensitivity can be enabled with the "-C" flag.
/// Anybody who wants to assemble the generated code will need to be aware of this.
/// </summary>
public class GenTass64 : IGenerator {
private const string ASM_FILE_SUFFIX = "_64tass.S"; // must start with underscore
private const int MAX_OPERAND_LEN = 64;
// IGenerator
public DisasmProject Project { get; private set; }
// IGenerator
public Formatter SourceFormatter { get; private set; }
// IGenerator
public AppSettings Settings { get; private set; }
// IGenerator
public AssemblerQuirks Quirks { get; private set; }
// IGenerator
public LabelLocalizer Localizer { get { return mLocalizer; } }
/// <summary>
/// Working directory, i.e. where we write our output file(s).
/// </summary>
private string mWorkDirectory;
/// <summary>
/// If set, long labels get their own line.
/// </summary>
private bool mLongLabelNewLine;
/// <summary>
/// Output column widths.
/// </summary>
private int[] mColumnWidths;
/// <summary>
/// Base filename. Typically the project file name without the ".dis65" extension.
/// </summary>
private string mFileNameBase;
/// <summary>
/// StringBuilder to use when composing a line. Held here to reduce allocations.
/// </summary>
private StringBuilder mLineBuilder = new StringBuilder(100);
/// <summary>
/// Label localization helper.
/// </summary>
private LabelLocalizer mLocalizer;
/// <summary>
/// Stream to send the output to.
/// </summary>
private StreamWriter mOutStream;
/// <summary>
/// If we output a ".logical", we will need a ".here" eventually.
/// </summary>
private bool mNeedHereOp;
/// <summary>
/// Holds detected version of configured assembler.
/// </summary>
private CommonUtil.Version mAsmVersion = CommonUtil.Version.NO_VERSION;
// Version we're coded against.
private static CommonUtil.Version V1_53 = new CommonUtil.Version(1, 53, 1515);
// Pseudo-op string constants.
private static PseudoOp.PseudoOpNames sDataOpNames = new PseudoOp.PseudoOpNames() {
EquDirective = "=",
OrgDirective = ".logical",
//RegWidthDirective // .as, .al, .xs, .xl
DefineData1 = ".byte",
DefineData2 = ".word",
DefineData3 = ".long",
DefineData4 = ".dword",
//DefineBigData2
//DefineBigData3
//DefineBigData4
Fill = ".fill",
//Dense // no equivalent, use .byte with comma-separated args
StrGeneric = ".text",
//StrReverse
StrNullTerm = ".null",
StrLen8 = ".ptext",
//StrLen16
//StrDci
//StrDciReverse
};
private const string HERE_PSEUDO_OP = ".here";
// IGenerator
public void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps,
out Formatter.FormatConfig formatConfig) {
pseudoOps = sDataOpNames;
formatConfig = new Formatter.FormatConfig();
SetFormatConfigValues(ref formatConfig);
}
// IGenerator
public void Configure(DisasmProject project, string workDirectory, string fileNameBase,
AssemblerVersion asmVersion, AppSettings settings) {
Debug.Assert(project != null);
Debug.Assert(!string.IsNullOrEmpty(workDirectory));
Debug.Assert(!string.IsNullOrEmpty(fileNameBase));
Project = project;
Quirks = new AssemblerQuirks();
mWorkDirectory = workDirectory;
mFileNameBase = fileNameBase;
Settings = settings;
mLongLabelNewLine = Settings.GetBool(AppSettings.SRCGEN_LONG_LABEL_NEW_LINE, false);
AssemblerConfig config = AssemblerConfig.GetConfig(settings,
AssemblerInfo.Id.Tass64);
mColumnWidths = (int[])config.ColumnWidths.Clone();
}
/// <summary>
/// Configures the assembler-specific format items.
/// </summary>
private void SetFormatConfigValues(ref Formatter.FormatConfig config) {
// Must be lower case when --case-sensitive is used.
config.mUpperOpcodes = false;
config.mUpperPseudoOpcodes = false;
config.mUpperOperandA = false;
config.mUpperOperandS = false;
config.mUpperOperandXY = false;
config.mBankSelectBackQuote = true;
config.mForceAbsOpcodeSuffix = string.Empty;
config.mForceLongOpcodeSuffix = string.Empty;
config.mForceDirectOperandPrefix = string.Empty;
config.mForceAbsOperandPrefix = "@w"; // word
config.mForceLongOperandPrefix = "@l"; // long
config.mEndOfLineCommentDelimiter = ";";
config.mFullLineCommentDelimiterBase = ";";
config.mBoxLineCommentDelimiter = ";";
config.mAllowHighAsciiCharConst = false;
config.mExpressionMode = Formatter.FormatConfig.ExpressionMode.Common;
}
// IGenerator
public List<string> GenerateSource(BackgroundWorker worker) {
List<string> pathNames = new List<string>(1);
string fileName = mFileNameBase + ASM_FILE_SUFFIX;
string pathName = Path.Combine(mWorkDirectory, fileName);
pathNames.Add(pathName);
Formatter.FormatConfig config = new Formatter.FormatConfig();
GenCommon.ConfigureFormatterFromSettings(Settings, ref config);
SetFormatConfigValues(ref config);
SourceFormatter = new Formatter(config);
string msg = string.Format(Res.Strings.PROGRESS_GENERATING_FMT, pathName);
worker.ReportProgress(0, msg);
mLocalizer = new LabelLocalizer(Project);
if (!Settings.GetBool(AppSettings.SRCGEN_DISABLE_LABEL_LOCALIZATION, false)) {
mLocalizer.LocalPrefix = "_";
mLocalizer.Analyze();
}
mLocalizer.MaskLeadingUnderscores();
// Use UTF-8 encoding, without a byte-order mark.
using (StreamWriter sw = new StreamWriter(pathName, false, new UTF8Encoding(false))) {
mOutStream = sw;
if (Settings.GetBool(AppSettings.SRCGEN_ADD_IDENT_COMMENT, false)) {
OutputLine(SourceFormatter.FullLineCommentDelimiter +
string.Format(Res.Strings.GENERATED_FOR_VERSION_FMT,
"64tass", V1_53, AsmTass64.OPTIONS));
}
GenCommon.Generate(this, sw, worker);
if (mNeedHereOp) {
OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(HERE_PSEUDO_OP),
string.Empty, string.Empty);
}
}
mOutStream = null;
return pathNames;
}
// IGenerator
public void OutputAsmConfig() {
CpuDef cpuDef = Project.CpuDef;
string cpuStr;
if (cpuDef.Type == CpuDef.CpuType.Cpu65816) {
cpuStr = "65816";
} else if (cpuDef.Type == CpuDef.CpuType.Cpu65C02) {
cpuStr = "65c02";
} else if (cpuDef.Type == CpuDef.CpuType.Cpu6502 && cpuDef.HasUndocumented) {
cpuStr = "6502i";
} else {
cpuStr = "6502";
}
OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(".cpu"),
'\"' + cpuStr + '\"', string.Empty);
}
// IGenerator
public string ModifyOpcode(int offset, OpDef op) {
if (op.IsUndocumented) {
if (Project.CpuDef.Type == CpuDef.CpuType.Cpu65C02) {
// none of the "LDD" stuff is handled
return null;
}
if ((op.Mnemonic == OpName.ANC && op.Opcode != 0x0b) ||
(op.Mnemonic == OpName.JAM && op.Opcode != 0x02)) {
// There are multiple opcodes that match the mnemonic. Output the
// mnemonic for the first one and hex for the rest.
return null;
} else if (op.Mnemonic == OpName.NOP || op.Mnemonic == OpName.DOP ||
op.Mnemonic == OpName.TOP) {
// the various undocumented no-ops aren't handled
return null;
} else if (op.Mnemonic == OpName.SBC) {
// this is the alternate reference to SBC
return null;
} else if (op == OpDef.OpSHA_DPIndIndexY) {
// not recognized ($93)
return null;
}
}
if (op == OpDef.OpBRK_StackInt || op == OpDef.OpCOP_StackInt ||
op == OpDef.OpWDM_WDM) {
// 64tass doesn't like these to have an operand. Output as hex.
return null;
}
return string.Empty; // indicate original is fine
}
// IGenerator
public void GenerateShortSequence(int offset, int length, out string opcode,
out string operand) {
Debug.Assert(length >= 1 && length <= 4);
// Use a comma-separated list of individual hex bytes.
opcode = sDataOpNames.DefineData1;
StringBuilder sb = new StringBuilder(length * 4);
for (int i = 0; i < length; i++) {
if (i != 0) {
sb.Append(',');
}
sb.Append(SourceFormatter.FormatHexValue(Project.FileData[offset + i], 2));
}
operand = sb.ToString();
}
// IGenerator
public void OutputDataOp(int offset) {
Formatter formatter = SourceFormatter;
byte[] data = Project.FileData;
Anattrib attr = Project.GetAnattrib(offset);
string labelStr = string.Empty;
if (attr.Symbol != null) {
labelStr = mLocalizer.ConvLabel(attr.Symbol.Label);
}
string commentStr = SourceFormatter.FormatEolComment(Project.Comments[offset]);
string opcodeStr, operandStr;
FormatDescriptor dfd = attr.DataDescriptor;
Debug.Assert(dfd != null);
int length = dfd.Length;
Debug.Assert(length > 0);
bool multiLine = false;
switch (dfd.FormatType) {
case FormatDescriptor.Type.Default:
if (length != 1) {
Debug.Assert(false);
length = 1;
}
opcodeStr = sDataOpNames.DefineData1;
int operand = RawData.GetWord(data, offset, length, false);
operandStr = formatter.FormatHexValue(operand, length * 2);
break;
case FormatDescriptor.Type.NumericLE:
opcodeStr = sDataOpNames.GetDefineData(length);
operand = RawData.GetWord(data, offset, length, false);
operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable,
mLocalizer.LabelMap, dfd, operand, length,
PseudoOp.FormatNumericOpFlags.None);
break;
case FormatDescriptor.Type.NumericBE:
opcodeStr = sDataOpNames.GetDefineBigData(length);
if (opcodeStr == null) {
// Nothing defined, output as comma-separated single-byte values.
GenerateShortSequence(offset, length, out opcodeStr, out operandStr);
} else {
operand = RawData.GetWord(data, offset, length, true);
operandStr = PseudoOp.FormatNumericOperand(formatter, Project.SymbolTable,
mLocalizer.LabelMap, dfd, operand, length,
PseudoOp.FormatNumericOpFlags.None);
}
break;
case FormatDescriptor.Type.Fill:
opcodeStr = sDataOpNames.Fill;
operandStr = length + "," + formatter.FormatHexValue(data[offset], 2);
break;
case FormatDescriptor.Type.Dense:
multiLine = true;
opcodeStr = operandStr = null;
OutputDenseHex(offset, length, labelStr, commentStr);
break;
case FormatDescriptor.Type.String:
multiLine = true;
opcodeStr = operandStr = null;
OutputString(offset, labelStr, commentStr);
break;
default:
opcodeStr = "???";
operandStr = "***";
break;
}
if (!multiLine) {
opcodeStr = formatter.FormatPseudoOp(opcodeStr);
OutputLine(labelStr, opcodeStr, operandStr, commentStr);
}
}
private void OutputDenseHex(int offset, int length, string labelStr, string commentStr) {
Formatter formatter = SourceFormatter;
byte[] data = Project.FileData;
StringBuilder sb = new StringBuilder(MAX_OPERAND_LEN);
string opcodeStr = formatter.FormatPseudoOp(sDataOpNames.DefineData1);
int maxPerLine = MAX_OPERAND_LEN / 4;
int numChunks = (length + maxPerLine - 1) / maxPerLine;
for (int chunk = 0; chunk < numChunks; chunk++) {
int chunkStart = chunk * maxPerLine;
int chunkEnd = Math.Min((chunk + 1) * maxPerLine, length);
for (int i = chunkStart; i < chunkEnd; i++) {
if (i != chunkStart) {
sb.Append(',');
}
sb.Append(formatter.FormatHexValue(data[offset + i], 2));
}
OutputLine(labelStr, opcodeStr, sb.ToString(), commentStr);
labelStr = commentStr = string.Empty;
sb.Clear();
}
}
/// <summary>
/// Outputs formatted data in an unformatted way, because the code generator couldn't
/// figure out how to do something better.
/// </summary>
private void OutputNoJoy(int offset, int length, string labelStr, string commentStr) {
byte[] data = Project.FileData;
Debug.Assert(length > 0);
Debug.Assert(offset >= 0 && offset < data.Length);
bool singleValue = true;
byte val = data[offset];
for (int i = 1; i < length; i++) {
if (data[offset + i] != val) {
singleValue = false;
break;
}
}
if (singleValue) {
string opcodeStr = SourceFormatter.FormatPseudoOp(sDataOpNames.Fill);
string operandStr = length + "," + SourceFormatter.FormatHexValue(val, 2);
OutputLine(labelStr, opcodeStr, operandStr, commentStr);
} else {
OutputDenseHex(offset, length, labelStr, commentStr);
}
}
// IGenerator
public void OutputEquDirective(string name, string valueStr, string comment) {
OutputLine(name, SourceFormatter.FormatPseudoOp(sDataOpNames.EquDirective),
valueStr, SourceFormatter.FormatEolComment(comment));
}
// IGenerator
public void OutputOrgDirective(int offset, int address) {
// 64tass separates the "compile offset", which determines where the output fits
// into the generated binary, and "program counter", which determines the code
// the assembler generates. Since we need to explicitly specify every byte in
// the output file, the compile offset isn't very useful. We want to set it once
// before the first line of code, then leave it alone.
//
// Any subsequent ORG changes are made to the program counter, and take the form
// of a pair of ops (.logical <addr> to open, .here to end). Omitting the .here
// causes an error.
if (offset == 0) {
// Set the "compile offset" to the initial address.
OutputLine("*", "=", SourceFormatter.FormatHexValue(Project.AddrMap.Get(0), 4),
string.Empty);
} else {
if (mNeedHereOp) {
OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(HERE_PSEUDO_OP),
string.Empty, string.Empty);
}
OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(sDataOpNames.OrgDirective),
SourceFormatter.FormatHexValue(address, 4), string.Empty);
mNeedHereOp = true;
}
}
// IGenerator
public void OutputRegWidthDirective(int offset, int prevM, int prevX, int newM, int newX) {
if (prevM != newM) {
string mop = (newM == 0) ? ".al" : ".as";
OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(mop),
string.Empty, string.Empty);
}
if (prevX != newX) {
string xop = (newX == 0) ? ".xl" : ".xs";
OutputLine(string.Empty, SourceFormatter.FormatPseudoOp(xop),
string.Empty, string.Empty);
}
}
// IGenerator
public void OutputLine(string fullLine) {
mOutStream.WriteLine(fullLine);
}
// IGenerator
public void OutputLine(string label, string opcode, string operand, string comment) {
// Break the line if the label is long and it's not a .EQ directive.
if (!string.IsNullOrEmpty(label) &&
!string.Equals(opcode, sDataOpNames.EquDirective,
StringComparison.InvariantCultureIgnoreCase)) {
if (mLongLabelNewLine && label.Length >= mColumnWidths[0]) {
mOutStream.WriteLine(label);
label = string.Empty;
}
}
mLineBuilder.Clear();
TextUtil.AppendPaddedString(mLineBuilder, label, mColumnWidths[0]);
TextUtil.AppendPaddedString(mLineBuilder, opcode, mColumnWidths[0] + mColumnWidths[1]);
TextUtil.AppendPaddedString(mLineBuilder, operand,
mColumnWidths[0] + mColumnWidths[1] + mColumnWidths[2]);
if (string.IsNullOrEmpty(comment)) {
// Trim trailing spaces off of opcode or operand. If they want trailing
// spaces at the end of a comment, that's fine.
CommonUtil.TextUtil.TrimEnd(mLineBuilder);
} else {
mLineBuilder.Append(comment);
}
mOutStream.WriteLine(mLineBuilder.ToString());
}
private void OutputString(int offset, string labelStr, string commentStr) {
// Normal ASCII strings are handled with a simple .text directive.
//
// CString and L8String have directives (.null, .ptext), but we can only use
// them if the string fits on one line and doesn't include delimiters.
//
// We could probably do something fancy with the character encoding options to
// make high-ASCII work nicely.
//
// We might be able to define a macro for DCI and Reverse.
Formatter formatter = SourceFormatter;
byte[] data = Project.FileData;
Anattrib attr = Project.GetAnattrib(offset);
FormatDescriptor dfd = attr.DataDescriptor;
Debug.Assert(dfd != null);
Debug.Assert(dfd.FormatType == FormatDescriptor.Type.String);
Debug.Assert(dfd.Length > 0);
bool highAscii = false;
int leadingBytes = 0;
int trailingBytes = 0;
bool showLeading = false;
bool showTrailing = false;
switch (dfd.FormatSubType) {
case FormatDescriptor.SubType.None:
highAscii = (data[offset] & 0x80) != 0;
break;
case FormatDescriptor.SubType.Dci:
highAscii = (data[offset] & 0x80) != 0;
trailingBytes = 1;
showTrailing = true;
break;
case FormatDescriptor.SubType.Reverse:
highAscii = (data[offset] & 0x80) != 0;
break;
case FormatDescriptor.SubType.DciReverse:
highAscii = (data[offset + dfd.Length - 1] & 0x80) != 0;
leadingBytes = 1;
showLeading = true;
break;
case FormatDescriptor.SubType.CString:
highAscii = (data[offset] & 0x80) != 0;
trailingBytes = 1;
showTrailing = true;
break;
case FormatDescriptor.SubType.L8String:
if (dfd.Length > 1) {
highAscii = (data[offset + 1] & 0x80) != 0;
}
leadingBytes = 1;
showLeading = true;
break;
case FormatDescriptor.SubType.L16String:
if (dfd.Length > 2) {
highAscii = (data[offset + 2] & 0x80) != 0;
}
leadingBytes = 2;
showLeading = true;
break;
default:
Debug.Assert(false);
return;
}
char delim = '"';
StringGather gath = null;
// Run the string through so we can see if it'll fit on one line. As a minor
// optimization, we skip this step for "generic" strings, which are probably
// the most common thing.
if (dfd.FormatSubType != FormatDescriptor.SubType.None || highAscii) {
gath = new StringGather(this, labelStr, "???", commentStr, delim,
delim, StringGather.ByteStyle.CommaSep, MAX_OPERAND_LEN, true);
FeedGath(gath, data, offset, dfd.Length, leadingBytes, showLeading,
trailingBytes, showTrailing);
Debug.Assert(gath.NumLinesOutput > 0);
}
string opcodeStr = formatter.FormatPseudoOp(sDataOpNames.StrGeneric);
switch (dfd.FormatSubType) {
case FormatDescriptor.SubType.None:
// TODO(someday): something fancy with encodings to handle high-ASCII text?
break;
case FormatDescriptor.SubType.Dci:
case FormatDescriptor.SubType.Reverse:
case FormatDescriptor.SubType.DciReverse:
// Fully configured above.
break;
case FormatDescriptor.SubType.CString:
if (gath.NumLinesOutput == 1 && !gath.HasDelimiter) {
opcodeStr = sDataOpNames.StrNullTerm;
showTrailing = false;
}
break;
case FormatDescriptor.SubType.L8String:
if (gath.NumLinesOutput == 1 && !gath.HasDelimiter) {
opcodeStr = sDataOpNames.StrLen8;
showLeading = false;
}
break;
case FormatDescriptor.SubType.L16String:
// Implement as macro?
break;
default:
Debug.Assert(false);
return;
}
if (highAscii) {
OutputNoJoy(offset, dfd.Length, labelStr, commentStr);
return;
}
// Create a new StringGather, with the final opcode choice.
gath = new StringGather(this, labelStr, opcodeStr, commentStr, delim,
delim, StringGather.ByteStyle.CommaSep, MAX_OPERAND_LEN, false);
FeedGath(gath, data, offset, dfd.Length, leadingBytes, showLeading,
trailingBytes, showTrailing);
}
/// <summary>
/// Feeds the bytes into the StringGather.
/// </summary>
private void FeedGath(StringGather gath, byte[] data, int offset, int length,
int leadingBytes, bool showLeading, int trailingBytes, bool showTrailing) {
int startOffset = offset;
int strEndOffset = offset + length - trailingBytes;
if (showLeading) {
while (leadingBytes-- > 0) {
gath.WriteByte(data[offset++]);
}
} else {
offset += leadingBytes;
}
for (; offset < strEndOffset; offset++) {
gath.WriteChar((char)(data[offset] & 0x7f));
}
while (showTrailing && trailingBytes-- > 0) {
gath.WriteByte(data[offset++]);
}
gath.Finish();
}
}
#endregion IGenerator
#region IAssembler
/// <summary>
/// Cross-assembler execution interface.
/// </summary>
public class AsmTass64 : IAssembler {
public const string OPTIONS = "--case-sensitive --nostart --long-address -Wall";
// Paths from generator.
private List<string> mPathNames;
// Directory to make current before executing assembler.
private string mWorkDirectory;
// IAssembler
public void GetExeIdentifiers(out string humanName, out string exeName) {
humanName = "64tass Assembler";
exeName = "64tass";
}
// IAssembler
public AssemblerConfig GetDefaultConfig() {
return new AssemblerConfig(string.Empty, new int[] { 8, 8, 11, 73 });
}
// IAssembler
public AssemblerVersion QueryVersion() {
AssemblerConfig config =
AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Tass64);
if (config == null || string.IsNullOrEmpty(config.ExecutablePath)) {
return null;
}
ShellCommand cmd = new ShellCommand(config.ExecutablePath, "--version",
Directory.GetCurrentDirectory(), null);
cmd.Execute();
if (string.IsNullOrEmpty(cmd.Stdout)) {
return null;
}
// Windows - Stdout: "64tass Turbo Assembler Macro V1.53.1515\r\n"
// Linux - Stdout: "64tass Turbo Assembler Macro V1.53.1515?\n"
const string PREFIX = "Macro V";
string str = cmd.Stdout;
int start = str.IndexOf(PREFIX);
int end = (start < 0) ? -1 : str.IndexOfAny(new char[] { '?', '\r', '\n' }, start + 1);
if (start < 0 || end < 0 || start + PREFIX.Length >= end) {
Debug.WriteLine("Couldn't find version in " + str);
return null;
}
start += PREFIX.Length;
string versionStr = str.Substring(start, end - start);
CommonUtil.Version version = CommonUtil.Version.Parse(versionStr);
if (!version.IsValid) {
return null;
}
return new AssemblerVersion(versionStr, version);
}
// IAssembler
public void Configure(List<string> pathNames, string workDirectory) {
// Clone pathNames, in case the caller decides to modify the original.
mPathNames = new List<string>(pathNames.Count);
foreach (string str in pathNames) {
mPathNames.Add(str);
}
mWorkDirectory = workDirectory;
}
// IAssembler
public AssemblerResults RunAssembler(BackgroundWorker worker) {
// Reduce input file to a partial path if possible. This is really just to make
// what we display to the user a little easier to read.
string pathName = mPathNames[0];
if (pathName.StartsWith(mWorkDirectory)) {
pathName = pathName.Remove(0, mWorkDirectory.Length + 1);
} else {
// Unexpected, but shouldn't be a problem.
Debug.WriteLine("NOTE: source file is not in work directory");
}
AssemblerConfig config =
AssemblerConfig.GetConfig(AppSettings.Global, AssemblerInfo.Id.Tass64);
if (string.IsNullOrEmpty(config.ExecutablePath)) {
Debug.WriteLine("Assembler not configured");
return null;
}
worker.ReportProgress(0, Res.Strings.PROGRESS_ASSEMBLING);
string outFileName = pathName.Substring(0, pathName.Length - 2);
// Wrap pathname in quotes in case it has spaces.
// (Do we need to shell-escape quotes in the pathName?)
ShellCommand cmd = new ShellCommand(config.ExecutablePath,
OPTIONS + " \"" + pathName + "\"" + " -o \"" + outFileName + "\"",
mWorkDirectory, null);
cmd.Execute();
// Can't really do anything with a "cancel" request.
// Output filename is the input filename without the ".S". Since the filename
// was generated by us we can be confident in the format.
string outputFile = mPathNames[0].Substring(0, mPathNames[0].Length - 2);
return new AssemblerResults(cmd.FullCommandLine, cmd.ExitCode, cmd.Stdout,
cmd.Stderr, outputFile);
}
}
#endregion IAssembler
}

View File

@ -0,0 +1,112 @@
/*
* 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.Web.Script.Serialization;
namespace SourceGenWPF.AsmGen {
/// <summary>
/// Assembler configuration holder. Serializes and deserializes information held in
/// application settings.
/// </summary>
public class AssemblerConfig {
// Public fields are deserialized from JSON. Changing the names will break compatibility.
/// <summary>
/// Path to cross-assembler executable. Will be null or empty if this assembler
/// is not configured.
/// </summary>
public string ExecutablePath { get; set; }
/// <summary>
/// Column display widths.
/// </summary>
public int[] ColumnWidths { get; set; }
public const int NUM_COLUMNS = 4; // label, opcode, operand, comment
/// <summary>
/// Nullary constructor, for serialization.
/// </summary>
public AssemblerConfig() { }
/// <summary>
/// Constructor.
/// </summary>
/// <param name="exePath">Path to executable. May be empty.</param>
/// <param name="widths">Column widths.</param>
public AssemblerConfig(string exePath, int[] widths) {
if (exePath == null) {
throw new Exception("Bad exe path");
}
if (widths.Length != NUM_COLUMNS) {
throw new Exception("Bad widths.Length " + widths.Length);
}
ExecutablePath = exePath;
ColumnWidths = widths;
}
private static string GetSettingName(AssemblerInfo.Id id) {
return AppSettings.ASM_CONFIG_PREFIX + id.ToString();
}
/// <summary>
/// Creates a populated AssemblerConfig from the app settings for the specified ID.
/// If the assembler hasn't been configured yet, the default configuration object
/// will be returned.
/// </summary>
/// <param name="settings">Settings object to pull the values from.</param>
/// <param name="id">Assembler ID.</param>
/// <returns>The AssemblerConfig.</returns>
public static AssemblerConfig GetConfig(AppSettings settings, AssemblerInfo.Id id) {
string cereal = settings.GetString(GetSettingName(id), null);
if (string.IsNullOrEmpty(cereal)) {
IAssembler asm = AssemblerInfo.GetAssembler(id);
return asm.GetDefaultConfig();
}
JavaScriptSerializer ser = new JavaScriptSerializer();
try {
AssemblerConfig config = ser.Deserialize<AssemblerConfig>(cereal);
if (config.ColumnWidths == null || config.ColumnWidths.Length != NUM_COLUMNS) {
throw new Exception("Bad column widths");
}
if (config.ExecutablePath == null) {
throw new Exception("Missing exe path");
}
return config;
} catch (Exception ex) {
Debug.WriteLine("AssemblerConfig deserialization failed: " + ex.Message);
return null;
}
}
/// <summary>
/// Updates the assembler settings for the specified ID.
/// </summary>
/// <param name="settings">Settings object to update.</param>
/// <param name="id">Assembler ID.</param>
/// <param name="config">Asm configuration.</param>
public static void SetConfig(AppSettings settings, AssemblerInfo.Id id,
AssemblerConfig config) {
JavaScriptSerializer ser = new JavaScriptSerializer();
string cereal = ser.Serialize(config);
settings.SetString(GetSettingName(id), cereal);
}
}
}

View File

@ -0,0 +1,170 @@
/*
* 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.AsmGen {
/// <summary>
/// Static information on assemblers supported by SourceGen. This is relevant for both
/// assembly source generation and assembler execution. Nothing here is affected
/// by whether or not the assembler in question is actually installed.
/// </summary>
public class AssemblerInfo {
/// <summary>
/// Enumeration of supported assemblers. Alphabetical order looks nicest.
/// </summary>
public enum Id {
Unknown = 0,
Tass64,
Cc65,
Merlin32,
}
/// <summary>
/// Static information for all known assemblers.
///
/// The AsmType argument may be null. This is useful for non-cross assemblers.
/// </summary>
private static AssemblerInfo[] sInfo = new AssemblerInfo[] {
new AssemblerInfo(Id.Unknown, "???", null, null),
new AssemblerInfo(Id.Tass64, "64tass", typeof(GenTass64), typeof(AsmTass64)),
new AssemblerInfo(Id.Cc65, "cc65", typeof(GenCc65), typeof(AsmCc65)),
new AssemblerInfo(Id.Merlin32, "Merlin 32", typeof(GenMerlin32), typeof(AsmMerlin32)),
};
/// <summary>
/// Identifier.
/// </summary>
public Id AssemblerId { get; private set; }
/// <summary>
/// Human-readable name.
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Type of generator class.
/// </summary>
public Type GenType { get; private set; }
/// <summary>
/// Type of assembler class.
/// </summary>
public Type AsmType { get; private set; }
private AssemblerInfo(Id id, string name, Type genType, Type asmType) {
AssemblerId = id;
Name = name;
GenType = genType;
AsmType = asmType;
}
/// <summary>
/// Returns an AssemblerInfo object for the specified id.
/// </summary>
/// <param name="id">Assembler identifier.</param>
/// <returns>Reference to AssemblerInfo object.</returns>
public static AssemblerInfo GetAssemblerInfo(Id id) {
return sInfo[(int)id];
}
/// <summary>
/// Generator factory method.
/// </summary>
/// <param name="id">ID of assembler to return generator instance for.</param>
/// <returns>New source generator object.</returns>
public static IGenerator GetGenerator(Id id) {
Type genType = sInfo[(int)id].GenType;
if (genType == null) {
Debug.Assert(false); // unexpected for generator
return null;
} else {
return (IGenerator)Activator.CreateInstance(genType);
}
}
/// <summary>
/// Assembler factory method.
/// </summary>
/// <param name="id">ID of assembler to return assembler instance for.</param>
/// <returns>New assembler interface object.</returns>
public static IAssembler GetAssembler(Id id) {
Type asmType = sInfo[(int)id].AsmType;
if (asmType == null) {
return null;
} else {
return (IAssembler)Activator.CreateInstance(asmType);
}
}
/// <summary>
/// Provides a way to iterate through the set of known assemblers. This is probably
/// YAGNI -- we could just return the array -- but it would allow us to apply filters,
/// e.g. strip out assemblers that don't support 65816 code when that's the selected
/// CPU definition.
/// </summary>
private class AssemblerInfoIterator : IEnumerator<AssemblerInfo> {
private int mIndex = -1;
public AssemblerInfo Current {
get {
if (mIndex < 0) {
// not started
return null;
}
return sInfo[mIndex];
}
}
object IEnumerator.Current {
get {
return Current;
}
}
public void Dispose() { }
public bool MoveNext() {
if (mIndex < 0) {
// skip element 0 (Unknown)
mIndex = 1;
} else {
mIndex++;
if (mIndex >= sInfo.Length) {
return false;
}
}
return true;
}
public void Reset() {
mIndex = -1;
}
}
public static IEnumerator<AssemblerInfo> GetInfoEnumerator() {
return new AssemblerInfoIterator();
}
public override string ToString() {
return "Asm " + ((int)AssemblerId).ToString() + ": " + Name;
}
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace SourceGenWPF.AsmGen {
public class AssemblerVersion {
/// <summary>
/// Version string reported by the assembler. Retained mostly for debugging.
/// </summary>
public string VersionStr { get; private set; }
/// <summary>
/// Version string converted to a Version object. For very complex version strings,
/// some information may be lost in the conversion.
/// </summary>
public CommonUtil.Version Version { get; private set; }
//// Command pathname and modification date. Useful for caching values.
//private string ExeName { get; set; }
//private DateTime ExeModWhen { get; set; }
public AssemblerVersion(string versionStr, CommonUtil.Version version) {
VersionStr = versionStr;
Version = version;
}
public static AssemblerVersion GetVersion(AssemblerInfo.Id id) {
IAssembler asm = AssemblerInfo.GetAssembler(id);
if (asm == null) {
Debug.WriteLine("Assembler " + id + " not configured");
return null;
}
return asm.QueryVersion();
}
public override string ToString() {
return "['" + VersionStr + "'/" + Version + "]";
}
}
/// <summary>
/// Maintains a cache of the versions of installed assemblers.
/// </summary>
public static class AssemblerVersionCache {
private static Dictionary<AssemblerInfo.Id, AssemblerVersion> sVersions =
new Dictionary<AssemblerInfo.Id, AssemblerVersion>();
private static bool sQueried = false;
/// <summary>
/// Queries the versions from all known assemblers, replacing any previously held data.
/// </summary>
public static void QueryVersions() {
IEnumerator<AssemblerInfo> iter = AssemblerInfo.GetInfoEnumerator();
while (iter.MoveNext()) {
AssemblerInfo.Id id = iter.Current.AssemblerId;
if (id == AssemblerInfo.Id.Unknown) {
continue;
}
AssemblerVersion vers = null;
IAssembler asm = AssemblerInfo.GetAssembler(id);
if (asm != null) {
vers = asm.QueryVersion();
}
Debug.WriteLine("Asm version query: " + id + "=" + vers);
sVersions[id] = vers;
}
sQueried = true;
}
/// <summary>
/// Returns the version information, or null if the query failed for this assembler.
/// </summary>
/// <param name="id">Assembler identifier.</param>
/// <returns>Version info.</returns>
public static AssemblerVersion GetVersion(AssemblerInfo.Id id) {
if (!sQueried) {
QueryVersions();
}
if (sVersions.TryGetValue(id, out AssemblerVersion vers)) {
return vers;
} else {
return null;
}
}
}
}

View File

@ -0,0 +1,371 @@
/*
* 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.ComponentModel;
using System.Diagnostics;
using System.IO;
using Asm65;
namespace SourceGenWPF.AsmGen {
public class GenCommon {
/// <summary>
/// Generates assembly source.
///
/// This code is common to all generators.
/// </summary>
/// <param name="gen">Reference to generator object (presumably the caller).</param>
/// <param name="sw">Text output sink.</param>
/// <param name="worker">Background worker object, for progress updates and
/// cancelation requests.</param>
public static void Generate(IGenerator gen, StreamWriter sw, BackgroundWorker worker) {
DisasmProject proj = gen.Project;
Formatter formatter = gen.SourceFormatter;
int offset = 0;
bool doAddCycles = gen.Settings.GetBool(AppSettings.SRCGEN_SHOW_CYCLE_COUNTS, false);
GenerateHeader(gen, sw);
// Used for M/X flag tracking.
StatusFlags prevFlags = StatusFlags.AllIndeterminate;
int lastProgress = 0;
while (offset < proj.FileData.Length) {
Anattrib attr = proj.GetAnattrib(offset);
if (attr.IsInstructionStart && offset > 0 &&
proj.GetAnattrib(offset - 1).IsData) {
// Transition from data to code. (Don't add blank line for inline data.)
gen.OutputLine(string.Empty);
}
// Long comments come first.
if (proj.LongComments.TryGetValue(offset, out MultiLineComment longComment)) {
List<string> formatted = longComment.FormatText(formatter, string.Empty);
foreach (string str in formatted) {
gen.OutputLine(str);
}
}
// Check for address change.
int orgAddr = proj.AddrMap.Get(offset);
if (orgAddr >= 0) {
gen.OutputOrgDirective(offset, orgAddr);
}
if (attr.IsInstructionStart) {
// Generate M/X reg width directive, if necessary.
// NOTE: we can suppress the initial directive if we know what the
// target assembler's default assumption is. Probably want to handle
// that in the ORG output handler.
if (proj.CpuDef.HasEmuFlag) {
StatusFlags curFlags = attr.StatusFlags;
curFlags.M = attr.StatusFlags.ShortM ? 1 : 0;
curFlags.X = attr.StatusFlags.ShortX ? 1 : 0;
if (curFlags.M != prevFlags.M || curFlags.X != prevFlags.X) {
// changed, output directive
gen.OutputRegWidthDirective(offset, prevFlags.M, prevFlags.X,
curFlags.M, curFlags.X);
}
prevFlags = curFlags;
}
// Look for embedded instructions.
int len;
for (len = 1; len < attr.Length; len++) {
if (proj.GetAnattrib(offset + len).IsInstructionStart) {
break;
}
}
// Output instruction.
GenerateInstruction(gen, sw, offset, len, doAddCycles);
if (attr.DoesNotContinue) {
gen.OutputLine(string.Empty);
}
offset += len;
} else {
gen.OutputDataOp(offset);
offset += attr.Length;
}
// Update progress meter. We don't want to spam it, so just ping it 10x.
int curProgress = (offset * 10) / proj.FileData.Length;
if (lastProgress != curProgress) {
if (worker.CancellationPending) {
Debug.WriteLine("GenCommon got cancellation request");
return;
}
lastProgress = curProgress;
worker.ReportProgress(curProgress * 10);
//System.Threading.Thread.Sleep(500);
}
}
}
private static void GenerateHeader(IGenerator gen, StreamWriter sw) {
DisasmProject proj = gen.Project;
Formatter formatter = gen.SourceFormatter;
// Check for header comment.
if (proj.LongComments.TryGetValue(LineListGen.Line.HEADER_COMMENT_OFFSET,
out MultiLineComment headerComment)) {
List<string> formatted = headerComment.FormatText(formatter, string.Empty);
foreach (string str in formatted) {
gen.OutputLine(str);
}
}
gen.OutputAsmConfig();
// Format symbols.
foreach (DefSymbol defSym in proj.ActiveDefSymbolList) {
// Use an operand length of 1 so things are shown as concisely as possible.
string valueStr = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable,
gen.Localizer.LabelMap, defSym.DataDescriptor, defSym.Value, 1,
PseudoOp.FormatNumericOpFlags.None);
gen.OutputEquDirective(defSym.Label, valueStr, defSym.Comment);
}
// If there was at least one symbol, output a blank line.
if (proj.ActiveDefSymbolList.Count != 0) {
gen.OutputLine(string.Empty);
}
}
private static void GenerateInstruction(IGenerator gen, StreamWriter sw, int offset,
int instrBytes, bool doAddCycles) {
DisasmProject proj = gen.Project;
Formatter formatter = gen.SourceFormatter;
byte[] data = proj.FileData;
Anattrib attr = proj.GetAnattrib(offset);
string labelStr = string.Empty;
if (attr.Symbol != null) {
labelStr = gen.Localizer.ConvLabel(attr.Symbol.Label);
}
OpDef op = proj.CpuDef.GetOpDef(data[offset]);
int operand = op.GetOperand(data, offset, attr.StatusFlags);
int instrLen = op.GetLength(attr.StatusFlags);
OpDef.WidthDisambiguation wdis = OpDef.WidthDisambiguation.None;
if (op.IsWidthPotentiallyAmbiguous) {
wdis = OpDef.GetWidthDisambiguation(instrLen, operand);
}
if (gen.Quirks.SinglePassAssembler && wdis == OpDef.WidthDisambiguation.None &&
(op.AddrMode == OpDef.AddressMode.DP ||
op.AddrMode == OpDef.AddressMode.DPIndexX) ||
op.AddrMode == OpDef.AddressMode.DPIndexY) {
// Could be a forward reference to a direct-page label.
if (IsForwardLabelReference(gen, offset)) {
wdis = OpDef.WidthDisambiguation.ForceDirect;
}
}
string opcodeStr = formatter.FormatOpcode(op, wdis);
string formattedOperand = null;
int operandLen = instrLen - 1;
PseudoOp.FormatNumericOpFlags opFlags = PseudoOp.FormatNumericOpFlags.None;
bool isPcRelBankWrap = false;
// Tweak branch instructions. We want to show the absolute address rather
// than the relative offset (which happens with the OperandAddress assignment
// below), and 1-byte branches should always appear as a 4-byte hex value.
if (op.AddrMode == OpDef.AddressMode.PCRel) {
Debug.Assert(attr.OperandAddress >= 0);
operandLen = 2;
opFlags = PseudoOp.FormatNumericOpFlags.IsPcRel;
} else if (op.AddrMode == OpDef.AddressMode.PCRelLong ||
op.AddrMode == OpDef.AddressMode.StackPCRelLong) {
opFlags = PseudoOp.FormatNumericOpFlags.IsPcRel;
} else if (op.AddrMode == OpDef.AddressMode.Imm ||
op.AddrMode == OpDef.AddressMode.ImmLongA ||
op.AddrMode == OpDef.AddressMode.ImmLongXY) {
opFlags = PseudoOp.FormatNumericOpFlags.HasHashPrefix;
}
if (opFlags == PseudoOp.FormatNumericOpFlags.IsPcRel) {
int branchDist = attr.Address - attr.OperandAddress;
isPcRelBankWrap = branchDist > 32767 || branchDist < -32768;
}
// 16-bit operands outside bank 0 need to include the bank when computing
// symbol adjustment.
int operandForSymbol = operand;
if (attr.OperandAddress >= 0) {
operandForSymbol = attr.OperandAddress;
}
// Check Length to watch for bogus descriptors. (ApplyFormatDescriptors() should
// now be screening bad descriptors out, so we may not need the Length test.)
if (attr.DataDescriptor != null && attr.Length == attr.DataDescriptor.Length) {
// Format operand as directed.
if (op.AddrMode == OpDef.AddressMode.BlockMove) {
// Special handling for the double-operand block move.
string opstr1 = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable,
gen.Localizer.LabelMap, attr.DataDescriptor, operand >> 8, 1,
PseudoOp.FormatNumericOpFlags.None);
string opstr2 = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable,
gen.Localizer.LabelMap, attr.DataDescriptor, operand & 0xff, 1,
PseudoOp.FormatNumericOpFlags.None);
if (gen.Quirks.BlockMoveArgsReversed) {
string tmp = opstr1;
opstr1 = opstr2;
opstr2 = tmp;
}
formattedOperand = opstr1 + "," + opstr2;
} else {
formattedOperand = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable,
gen.Localizer.LabelMap, attr.DataDescriptor,
operandForSymbol, operandLen, opFlags);
}
} else {
// Show operand value in hex.
if (op.AddrMode == OpDef.AddressMode.BlockMove) {
int arg1, arg2;
if (gen.Quirks.BlockMoveArgsReversed) {
arg1 = operand & 0xff;
arg2 = operand >> 8;
} else {
arg1 = operand >> 8;
arg2 = operand & 0xff;
}
formattedOperand = formatter.FormatHexValue(arg1, 2) + "," +
formatter.FormatHexValue(arg2, 2);
} else {
if (operandLen == 2) {
// This is necessary for 16-bit operands, like "LDA abs" and "PEA val",
// when outside bank zero. The bank is included in the operand address,
// but we don't want to show it here.
operandForSymbol &= 0xffff;
}
formattedOperand = formatter.FormatHexValue(operandForSymbol, operandLen * 2);
}
}
string operandStr = formatter.FormatOperand(op, formattedOperand, wdis);
string eolComment = proj.Comments[offset];
if (doAddCycles) {
bool branchCross = (attr.Address & 0xff00) != (operandForSymbol & 0xff00);
int cycles = proj.CpuDef.GetCycles(op.Opcode, attr.StatusFlags, attr.BranchTaken,
branchCross);
if (cycles > 0) {
eolComment = cycles.ToString() + " " + eolComment;
} else {
eolComment = (-cycles).ToString() + "+ " + eolComment;
}
}
string commentStr = formatter.FormatEolComment(eolComment);
string replMnemonic = gen.ModifyOpcode(offset, op);
if (attr.Length != instrBytes) {
// This instruction has another instruction inside it. Throw out what we
// computed and just output as bytes.
gen.GenerateShortSequence(offset, instrBytes, out opcodeStr, out operandStr);
} else if (isPcRelBankWrap && gen.Quirks.NoPcRelBankWrap) {
// Some assemblers have trouble generating PC-relative operands that wrap
// around the bank. Output as raw hex.
gen.GenerateShortSequence(offset, instrBytes, out opcodeStr, out operandStr);
} else if (op.AddrMode == OpDef.AddressMode.BlockMove &&
gen.Quirks.BlockMoveArgsReversed) {
// On second thought, just don't even output the wrong thing.
gen.GenerateShortSequence(offset, instrBytes, out opcodeStr, out operandStr);
} else if (replMnemonic == null) {
// No mnemonic exists for this opcode.
gen.GenerateShortSequence(offset, instrBytes, out opcodeStr, out operandStr);
} else if (replMnemonic != string.Empty) {
// A replacement mnemonic has been provided.
opcodeStr = formatter.FormatMnemonic(replMnemonic, wdis);
}
gen.OutputLine(labelStr, opcodeStr, operandStr, commentStr);
// Assemblers like Merlin32 try to be helpful and track SEP/REP, but they do the
// wrong thing if we're in emulation mode. Force flags back to short.
if (proj.CpuDef.HasEmuFlag && gen.Quirks.TracksSepRepNotEmu && op == OpDef.OpREP_Imm) {
if ((operand & 0x30) != 0 && attr.StatusFlags.E == 1) {
gen.OutputRegWidthDirective(offset, 0, 0, 1, 1);
}
}
}
/// <summary>
/// Determines whether the instruction at the specified offset has an operand that is
/// a forward reference. This only matters for single-pass assemblers.
/// </summary>
/// <param name="gen">Source generator reference.</param>
/// <param name="offset">Offset of instruction opcode.</param>
/// <returns>True if the instruction's operand is a forward reference to a label.</returns>
private static bool IsForwardLabelReference(IGenerator gen, int offset) {
DisasmProject proj = gen.Project;
Debug.Assert(proj.GetAnattrib(offset).IsInstructionStart);
FormatDescriptor dfd = proj.GetAnattrib(offset).DataDescriptor;
if (dfd == null || !dfd.HasSymbol) {
return false;
}
int labelOffset = proj.FindLabelOffsetByName(dfd.SymbolRef.Label);
if (labelOffset <= offset) {
// Doesn't exist, or is backward reference.
return false;
}
return true;
}
/// <summary>
/// Configures some common format config items from the app settings. Uses a
/// passed-in settings object, rather than the global settings.
/// </summary>
/// <param name="settings">Application settings.</param>
/// <param name="config">Format config struct.</param>
public static void ConfigureFormatterFromSettings(AppSettings settings,
ref Formatter.FormatConfig config) {
config.mUpperHexDigits =
settings.GetBool(AppSettings.FMT_UPPER_HEX_DIGITS, false);
config.mUpperOpcodes =
settings.GetBool(AppSettings.FMT_UPPER_OP_MNEMONIC, false);
config.mUpperPseudoOpcodes =
settings.GetBool(AppSettings.FMT_UPPER_PSEUDO_OP_MNEMONIC, false);
config.mUpperOperandA =
settings.GetBool(AppSettings.FMT_UPPER_OPERAND_A, false);
config.mUpperOperandS =
settings.GetBool(AppSettings.FMT_UPPER_OPERAND_S, false);
config.mUpperOperandXY =
settings.GetBool(AppSettings.FMT_UPPER_OPERAND_XY, false);
config.mSpacesBetweenBytes =
settings.GetBool(AppSettings.FMT_SPACES_BETWEEN_BYTES, false);
config.mAddSpaceLongComment =
settings.GetBool(AppSettings.FMT_ADD_SPACE_FULL_COMMENT, true);
config.mForceAbsOpcodeSuffix =
settings.GetString(AppSettings.FMT_OPCODE_SUFFIX_ABS, string.Empty);
config.mForceLongOpcodeSuffix =
settings.GetString(AppSettings.FMT_OPCODE_SUFFIX_LONG, string.Empty);
config.mForceAbsOperandPrefix =
settings.GetString(AppSettings.FMT_OPERAND_PREFIX_ABS, string.Empty);
config.mForceLongOperandPrefix =
settings.GetString(AppSettings.FMT_OPERAND_PREFIX_LONG, string.Empty);
string exprMode = settings.GetString(AppSettings.FMT_EXPRESSION_MODE, string.Empty);
config.mExpressionMode = Formatter.FormatConfig.ParseExpressionMode(exprMode);
}
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.ComponentModel;
namespace SourceGenWPF.AsmGen {
/// <summary>
/// Common interface for executing assemblers.
/// </summary>
public interface IAssembler {
/// <summary>
/// Gets identification strings for the executable. These are used when browsing for
/// the assembler binary.
/// </summary>
/// <param name="humanName">Human-readable name to show in the "open" dialog.</param>
/// <param name="exeName">Name of executable to find, without ".exe".</param>
void GetExeIdentifiers(out string humanName, out string exeName);
/// <summary>
/// Queries the assembler for its default configuration.
/// </summary>
/// <returns>Config object with default values.</returns>
AssemblerConfig GetDefaultConfig();
/// <summary>
/// Queries the assembler for its version. Assembler executable paths are queried from
/// the global settings object.
/// </summary>
/// <returns>Assembler version info, or null if query failed.</returns>
AssemblerVersion QueryVersion();
/// <summary>
/// Configures the object. Pass in the list of pathnames returned by IGenerator.Run(),
/// and the working directory to use for the shell command.
/// </summary>
/// <param name="pathNames">Assembler source pathnames.</param>
/// <param name="workDirectory">Working directory for shell command.</param>
void Configure(List<string> pathNames, string workDirectory);
/// <summary>
/// Executes the assembler. Must call Configure() first. Executed on background thread.
/// </summary>
/// <param name="worker">Async work object, used to report progress updates and
/// check for cancellation.</param>
/// <returns>Execution results, or null on internal failure.</returns>
AssemblerResults RunAssembler(BackgroundWorker worker);
}
/// <summary>
/// Set of values returned by the assembler.
/// </summary>
public class AssemblerResults {
public string CommandLine { get; private set; }
public int ExitCode { get; private set; }
public string Stdout { get; private set; }
public string Stderr { get; private set; }
public string OutputPathName { get; private set; }
public AssemblerResults(string commandLine, int exitCode, string stdout, string stderr,
string outputFile) {
CommandLine = commandLine;
ExitCode = exitCode;
Stdout = stdout;
Stderr = stderr;
OutputPathName = outputFile;
}
}
}

View File

@ -0,0 +1,192 @@
/*
* 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.ComponentModel;
using Asm65;
namespace SourceGenWPF.AsmGen {
/// <summary>
/// Common interface for generating assembler-specific source code.
/// </summary>
public interface IGenerator {
/// <summary>
/// Returns some strings and format options for use in for the display list, configurable
/// through the app settings "quick set" feature. These are not used when generating
/// source code.
///
/// This may be called on an unconfigured IGenerator.
/// </summary>
/// <param name="pseudoOps">Table of pseudo-op names.</param>
/// <param name="formatConfig">Format configuration.</param>
void GetDefaultDisplayFormat(out PseudoOp.PseudoOpNames pseudoOps,
out Formatter.FormatConfig formatConfig);
/// <summary>
/// Configure generator. Must be called before calling any other method or using
/// properties, unless otherwise noted.
/// </summary>
/// <param name="project">Project to generate source for.</param>
/// <param name="workDirectory">Directory in which to create output files.</param>
/// <param name="fileNameBase">Name to use as base for filenames.</param>
/// <param name="asmVersion">Version of assembler to target. Pass in null
/// to target latest known version.</param>
/// <param name="settings">App settings object.</param>
void Configure(DisasmProject project, string workDirectory, string fileNameBase,
AssemblerVersion asmVersion, AppSettings settings);
/// <summary>
/// Project object with file data and Anattribs.
/// </summary>
DisasmProject Project { get; }
/// <summary>
/// Source code formatter.
/// </summary>
Formatter SourceFormatter { get; }
/// <summary>
/// Application settings.
/// </summary>
AppSettings Settings { get; }
/// <summary>
/// Assembler-specific behavior. Used to handle quirky behavior for things that
/// are otherwise managed by common code.
/// </summary>
AssemblerQuirks Quirks { get; }
/// <summary>
/// Label localization object. Behavior is assembler-specific.
/// </summary>
LabelLocalizer Localizer { get; }
/// <summary>
/// Generates source files on a background thread. Method must not make any UI calls.
/// </summary>
/// <param name="worker">Async work object, used to report progress updates and
/// check for cancellation.</param>
/// <returns>List of pathnames of generated files.</returns>
List<string> GenerateSource(BackgroundWorker worker);
/// <summary>
/// Provides an opportunity for the assembler to replace a mnemonic with another, or
/// output an instruction as hex bytes.
/// </summary>
/// <param name="offset">Opcode offset.</param>
/// <param name="op">Opcode to replace.</param>
/// <returns>Replacement mnemonic, an empty string if the original is fine, or
/// null if the op is unsupported or broken and should be emitted as hex.</returns>
string ModifyOpcode(int offset, OpDef op);
/// <summary>
/// Generates an opcode/operand pair for a short sequence of bytes (1-4 bytes).
/// Does not produce any source output.
/// </summary>
/// <param name="offset">Offset to data.</param>
/// <param name="count">Number of bytes (1-4).</param>
/// <param name="opcode">Opcode mnemonic.</param>
/// <param name="operand">Formatted operand.</param>
void GenerateShortSequence(int offset, int length, out string opcode, out string operand);
/// <summary>
/// Outputs zero or more lines of assembler configuration. This comes after the
/// header comment but before any directives. Useful for configuring the CPU type
/// and assembler options.
/// </summary>
void OutputAsmConfig();
/// <summary>
/// Outputs one or more lines of data for the specified offset.
/// </summary>
/// <param name="offset">Offset to data.</param>
void OutputDataOp(int offset);
/// <summary>
/// Outputs an equate directive. The numeric value is already formatted.
/// </summary>
/// <param name="name">Symbol label.</param>
/// <param name="valueStr">Formatted value.</param>
/// <param name="comment">End-of-line comment.</param>
void OutputEquDirective(string name, string valueStr, string comment);
/// <summary>
/// Outputs a code origin directive.
/// </summary>
/// <param name="offset">Offset of code targeted to new address.</param>
/// <param name="address">24-bit address.</param>
void OutputOrgDirective(int offset, int address);
/// <summary>
/// Notify the assembler of a change in register width.
///
/// Merlin32 always sets both values (e.g. "MX %00"), cc65 sets each register
/// individually (".A16", ".I8"). We need to accommodate both styles.
/// </summary>
/// <param name="offset">Offset of change.</param>
/// <param name="prevM">Previous value for M flag.</param>
/// <param name="prevX">Previous value for X flag.</param>
/// <param name="newM">New value for M flag.</param>
/// <param name="newX">New value for X flag.</param>
void OutputRegWidthDirective(int offset, int prevM, int prevX, int newM, int newX);
/// <summary>
/// Output a line of source code. All elements must be fully formatted, except for
/// certain assembler-specific things like ':' on labels. The items will be padded
/// with spaces to fit specific column widths.
/// </summary>
/// <param name="label">Optional label.</param>
/// <param name="opcode">Opcode mnemonic.</param>
/// <param name="operand">Operand; may be empty.</param>
/// <param name="comment">Optional comment.</param>
void OutputLine(string label, string opcode, string operand, string comment);
/// <summary>
/// Output a line of source code. This will be output as-is.
/// </summary>
/// <param name="fullLine">Full text of line to outut.</param>
void OutputLine(string fullLine);
}
/// <summary>
/// Enumeration of quirky or buggy behavior that GenCommon needs to handle.
/// </summary>
public class AssemblerQuirks {
/// <summary>
/// Are the arguments to MVN/MVP reversed?
/// </summary>
public bool BlockMoveArgsReversed { get; set; }
/// <summary>
/// Does the assembler configure assembler widths based on SEP/REP, but doesn't
/// track the emulation bit?
/// </summary>
public bool TracksSepRepNotEmu { get; set; }
/// <summary>
/// Is the assembler unable to generate relative branches that wrap around banks?
/// (Note this affects long-distance BRLs that don't appear to wrap.)
/// </summary>
public bool NoPcRelBankWrap { get; set; }
/// <summary>
/// Is the assembler implemented as a single pass?
/// </summary>
public bool SinglePassAssembler { get; set; }
}
}

View File

@ -0,0 +1,386 @@
/*
* 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;
/*
Some assemblers support "local labels", with varying definitions of scope and features.
Generally speaking, local labels only need to be unique within a certain limited scope, and
they aren't included in end-of-assembly symbol lists.
One popular form defines its scope as being between two global labels. So this is allowed:
glob1 lda #$00
:local sta $00
glob2 lda #$01
:local sta $01
but this would cause an error:
glob1 lda #$00
:local sta $00
glob2 lda #$01
bne :local
because the local symbol table is cleared when a global symbol is encountered.
Another common form allows backward references to labels that don't go out of scope until
they're re-used. This is useful for short loops.
As a further limitation, assemblers seem to want the first label encountered in a program
to be global.
The Symbol.SymbolType enum allows a label to be defined as "local or global". We can output
these with the local-symbol syntax, potentially rewriting them to have non-unique names like
"loop", but we can't promote (demote?) a label to local unless there are no references to it
that cross a global label.
The cross-reference table we generate as part of the analysis process provides a full list of
label references, so we just need to iterate through the label list until we can't find
anything else that needs to be made global.
Because the definition of "local label" is somewhat assembler-specific, it's best to defer
this analysis to code generation time, when the specific characteristics of the target
assembler can be taken into account.
References to an offset can be numeric or symbolic. A purely numeric reference like "LDA $2000"
will always map to the offset associated with address $2000, but a symbolic reference might be
offset. For example, the LDA instruction could reference a label at $2008 as "LDA FOO-8".
The assembler cares about the symbolic references, not the actual offsets or addresses. For
this reason we can ignore references to an address with a label if those references don't
actually use the label. (One consequence of this is that formatting an operand as hex
eliminates it from the set of things for us to consider. Also, ORG directives have no effect
on the localizer.)
Labels that are marked as global, but to which there are no references, could in theory be
elided. To do this we would have to omit them from the generated code, which would be
annoying and weird if (say) the user added them to label an external entry point.
The eventual output of our efforts is a map from the original symbol name to the local symbol
name. This must be applied to both labels and operands.
*/
namespace SourceGenWPF.AsmGen {
public class LabelLocalizer {
/// <summary>
/// A pairing of an offset with a label string. (Essentially mAnattribs[n].Symbol
/// with all the fluff trimmed away.)
///
/// The label string isn't actually all that useful, since we can pull it back out
/// of anattrib, but it makes life a little easier during debugging. These get
/// put into a List, so switching to a plain int offset doesn't necessarily help us
/// much because the ints get boxed.
/// </summary>
private class OffsetLabel {
public int Offset { get; private set; }
public string Label { get; private set; }
public OffsetLabel(int offset, string label) {
Offset = offset;
Label = label;
}
public override string ToString() {
return "+" + Offset.ToString("x6") + "(" + Label + ")";
}
}
/// <summary>
/// A pair of offsets. An operand (instruction or data) at the source offset
/// references a label at the destination offset.
/// </summary>
private class OffsetPair {
public int SrcOffset { get; private set; } // offset from which reference is made
public int DstOffset { get; private set; } // offset being referred to
public OffsetPair(int src, int dst) {
SrcOffset = src;
DstOffset = dst;
}
public override string ToString() {
return "src=+" + SrcOffset.ToString("x6") + " dst=+" + DstOffset.ToString("x6");
}
}
/// <summary>
/// Map from label string to local label string. This will be null until Analyze()
/// has executed.
/// </summary>
public Dictionary<string, string> LabelMap { get; private set; }
/// <summary>
/// String to prefix to local labels. Usually a single character, like ':' or '@'.
/// </summary>
public string LocalPrefix { get; set; }
/// <summary>
/// Project reference.
/// </summary>
private DisasmProject mProject;
// Work state.
private List<OffsetLabel> mGlobalLabels = new List<OffsetLabel>();
private List<OffsetPair> mOffsetPairs = new List<OffsetPair>();
private BitArray mGlobalFlags;
public LabelLocalizer(DisasmProject project) {
mProject = project;
mGlobalFlags = new BitArray(mProject.FileDataLength);
LocalPrefix = "!?";
}
/// <summary>
/// Applies the LabelMap to the label. If the LabelMap is null, or does not have an
/// entry for the label, the original label is returned.
/// </summary>
/// <param name="label">Label to convert.</param>
/// <returns>New label, or original label.</returns>
public string ConvLabel(string label) {
if (LabelMap != null) {
if (LabelMap.TryGetValue(label, out string newLabel)) {
label = newLabel;
}
}
return label;
}
/// <summary>
/// Analyzes labels to identify which ones may be treated as non-global.
/// </summary>
public void Analyze() {
Debug.Assert(LocalPrefix.Length > 0);
mGlobalFlags.SetAll(false);
// Currently we only support the "local labels have scope that ends at a global
// label" variety. The basic idea is to start by assuming that everything not
// explicitly marked global is local, and then identify situations like this:
//
// lda :local
// global eor #$ff
// :local sta $00
//
// The reference crosses a global label, so the "target" label must be made global.
// This can have ripple effects, so we have to iterate. Note it doesn't matter
// whether "global" is referenced anywhere.
//
// The current algorithm uses a straightforward O(n^2) approach.
// Step 1: generate source/target pairs and global label list
GenerateLists();
// Step 2: walk through the list of global symbols, identifying source/target
// pairs that cross them. If a pair matches, the target label is added to the
// mGlobalLabels list, and removed from the pair list.
for (int index = 0; index < mGlobalLabels.Count; index++) {
FindIntersectingPairs(mGlobalLabels[index]);
}
// Step 3: for each local label, add an entry to the map with the appropriate
// local-label syntax.
LabelMap = new Dictionary<string, string>();
for (int i = 0; i < mProject.FileDataLength; i++) {
if (mGlobalFlags[i]) {
continue;
}
Symbol sym = mProject.GetAnattrib(i).Symbol;
if (sym == null) {
continue;
}
LabelMap[sym.Label] = LocalPrefix + sym.Label;
}
// Take out the trash.
mGlobalLabels.Clear();
mOffsetPairs.Clear();
}
/// <summary>
/// Generates the initial mGlobalFlags and mGlobalLabels lists, as well as the
/// full cross-reference pair list.
/// </summary>
private void GenerateLists() {
// For every offset that has a label, add an entry to the source/target pair list
// for every offset that references it.
//
// If the label isn't marked as "local or global", add it to the global-label list.
//
// The first label encountered is always treated as global. Note it may not appear
// at offset zero.
bool first = true;
for (int i = 0; i < mProject.FileDataLength; i++) {
Symbol sym = mProject.GetAnattrib(i).Symbol;
if (sym == null) {
// No label at this offset.
continue;
}
if (first || sym.SymbolType != Symbol.Type.LocalOrGlobalAddr) {
first = false;
mGlobalFlags[i] = true;
mGlobalLabels.Add(new OffsetLabel(i, sym.Label));
// Don't add to pairs list.
continue;
}
// If nothing actually references this label, the xref set will be empty.
XrefSet xrefs = mProject.GetXrefSet(i);
if (xrefs != null) {
foreach (XrefSet.Xref xref in xrefs) {
if (!xref.IsSymbolic) {
continue;
}
mOffsetPairs.Add(new OffsetPair(xref.Offset, i));
}
}
}
}
/// <summary>
/// Identifies all label reference pairs that cross the specified global label. When
/// a matching pair is found, the pair's destination label is marked as global and
/// added to the global label list.
/// </summary>
/// <param name="glabel">Global label of interest.</param>
private void FindIntersectingPairs(OffsetLabel glabel) {
Debug.Assert(mGlobalFlags[glabel.Offset]);
int globOffset = glabel.Offset;
for (int i = 0; i < mOffsetPairs.Count; i++) {
OffsetPair pair = mOffsetPairs[i];
// If the destination was marked global earlier, remove and ignore this entry.
// Note this also means that pair.DstOffset != label.Offset.
if (mGlobalFlags[pair.DstOffset]) {
mOffsetPairs.RemoveAt(i);
i--;
continue;
}
// Check to see if the global label falls between the source and destination
// offsets.
//
// If the reference source is itself a global label, it can reference local
// labels forward, but not backward. We need to take that into account for
// the case where label.Offset==pair.SrcOffset.
bool intersect;
if (pair.SrcOffset < pair.DstOffset) {
// Forward reference. src==glob is ok
intersect = pair.SrcOffset < globOffset && pair.DstOffset >= globOffset;
} else {
// Backward reference. src==glob is bad
intersect = pair.SrcOffset >= globOffset && pair.DstOffset <= globOffset;
}
if (intersect) {
//Debug.WriteLine("Global " + glabel + " btwn " + pair + " (" +
// mProject.GetAnattrib(pair.DstOffset).Symbol.Label + ")");
// Change the destination label to global.
mGlobalFlags[pair.DstOffset] = true;
mGlobalLabels.Add(new OffsetLabel(pair.DstOffset,
mProject.GetAnattrib(pair.DstOffset).Symbol.Label));
// Carefully remove it from the list we're iterating through.
mOffsetPairs.RemoveAt(i);
i--;
}
}
}
/// <summary>
/// Adjusts the label map so that only local variables start with an underscore ('_').
/// This is necessary for assemblers like 64tass that use a leading underscore to
/// indicate that a label should be local.
///
/// This may be called even if label localization is disabled. In that case we just
/// create an empty label map and populate as needed.
///
/// Only call this if underscores are used to indicate local labels.
/// </summary>
public void MaskLeadingUnderscores() {
bool allGlobal = false;
if (LabelMap == null) {
allGlobal = true;
LabelMap = new Dictionary<string, string>();
}
// Throw out the original local label generation.
LabelMap.Clear();
// Use this to test for uniqueness. We add all labels here as we go, not just the
// ones being remapped. For each label we either add the original or the localized
// form.
SortedList<string, string> allLabels = new SortedList<string, string>();
for (int i = 0; i < mProject.FileDataLength; i++) {
Symbol sym = mProject.GetAnattrib(i).Symbol;
if (sym == null) {
// No label at this offset.
continue;
}
string newLabel;
if (allGlobal || mGlobalFlags[i]) {
// Global symbol. Don't let it start with '_'.
if (sym.Label.StartsWith("_")) {
// There's an underscore here that was added by the user. Stick some
// other character in front.
newLabel = "X" + sym.Label;
} else {
// No change needed.
newLabel = sym.Label;
}
} else {
// Local symbol.
if (sym.Label.StartsWith("_")) {
// The original starts with one or more underscores. Adding another
// will create a "__" label, which is reserved in 64tass.
newLabel = "_X" + sym.Label;
} else {
newLabel = "_" + sym.Label;
}
}
// Make sure it's unique.
string uniqueLabel = newLabel;
int uval = 1;
while (allLabels.ContainsKey(uniqueLabel)) {
uniqueLabel = newLabel + uval.ToString();
}
allLabels.Add(uniqueLabel, uniqueLabel);
// If it's different, add it to the label map.
if (sym.Label != uniqueLabel) {
LabelMap.Add(sym.Label, uniqueLabel);
}
}
Debug.WriteLine("UMAP: allcount=" + allLabels.Count + " mapcount=" + LabelMap.Count);
}
}
}

View File

@ -0,0 +1,227 @@
/*
* 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.AsmGen {
/// <summary>
/// Multi-line string gatherer. Accumulates characters and raw bytes, emitting
/// them when we have a full operand's worth.
///
/// If the delimiter character appears, it will be output inline as a raw byte.
/// The low-ASCII string ['hello'world'] will become [27,'hello',27,'world',27]
/// (or something similar).
/// </summary>
public class StringGather {
// Inputs.
public IGenerator Gen { get; private set; }
public string Label { get; private set; }
public string Opcode { get; private set; }
public string Comment { get; private set; }
public char Delimiter { get; private set; }
public char DelimiterReplacement { get; private set; }
public ByteStyle ByteStyleX { get; private set; }
public int MaxOperandLen { get; private set; }
public bool IsTestRun { get; private set; }
public enum ByteStyle { DenseHex, CommaSep };
// Outputs.
public bool HasDelimiter { get; private set; }
public int NumLinesOutput { get; private set; }
private char[] mHexChars;
/// <summary>
/// Character collection buffer. The delimiters are written into the buffer
/// because they're mixed with bytes, particularly when we have to escape the
/// delimiter character. Strings might start or end with escaped delimiters,
/// so we don't add them until we have to.
private char[] mBuffer;
/// <summary>
/// Next available character position.
/// </summary>
private int mIndex = 0;
/// <summary>
/// State of the buffer, based on the last thing we added.
/// </summary>
private enum State {
Unknown = 0,
StartOfLine,
InQuote,
OutQuote
}
private State mState = State.StartOfLine;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="gen">Reference back to generator, for output function and
/// format options.</param>
/// <param name="label">Line label. Appears on first output line only.</param>
/// <param name="opcode">Opcode to use for all lines.</param>
/// <param name="comment">End-of-line comment. Appears on first output line
/// only.</param>
/// <param name="delimiter">String delimiter character.</param>
/// <param name="isTestRun">If true, no file output is produced.</param>
public StringGather(IGenerator gen, string label, string opcode,
string comment, char delimiter, char delimReplace, ByteStyle byteStyle,
int maxOperandLen, bool isTestRun) {
Gen = gen;
Label = label;
Opcode = opcode;
Comment = comment;
Delimiter = delimiter;
DelimiterReplacement = delimReplace;
ByteStyleX = byteStyle;
MaxOperandLen = maxOperandLen;
IsTestRun = isTestRun;
mBuffer = new char[MaxOperandLen];
mHexChars = Gen.SourceFormatter.HexDigits;
}
/// <summary>
/// Write a character into the buffer.
/// </summary>
/// <param name="ch">Character to add.</param>
public void WriteChar(char ch) {
Debug.Assert(ch >= 0 && ch <= 0xff);
if (ch == Delimiter) {
// Must write it as a byte.
HasDelimiter = true;
WriteByte((byte)DelimiterReplacement);
return;
}
// If we're at the start of a line, add delimiter, then new char.
// If we're inside quotes, just add the character. We must have space for
// two chars (new char, close quote).
// If we're outside quotes, add a comma and delimiter, then the character.
// We must have 4 chars remaining (comma, open quote, new char, close quote).
switch (mState) {
case State.StartOfLine:
mBuffer[mIndex++] = Delimiter;
break;
case State.InQuote:
if (mIndex + 2 > MaxOperandLen) {
Flush();
mBuffer[mIndex++] = Delimiter;
}
break;
case State.OutQuote:
if (mIndex + 4 > MaxOperandLen) {
Flush();
mBuffer[mIndex++] = Delimiter;
} else {
mBuffer[mIndex++] = ',';
mBuffer[mIndex++] = Delimiter;
}
break;
default:
Debug.Assert(false);
break;
}
mBuffer[mIndex++] = ch;
mState = State.InQuote;
}
/// <summary>
/// Write a hex value into the buffer.
/// </summary>
/// <param name="val">Value to add.</param>
public void WriteByte(byte val) {
// If we're at the start of a line, just output the byte.
// If we're inside quotes, emit a delimiter, comma, and the byte. We must
// have space for four (DenseHex) or five (CommaSep) chars.
// If we're outside quotes, add the byte. We must have two (DenseHex) or
// four (CommaSep) chars remaining.
switch (mState) {
case State.StartOfLine:
break;
case State.InQuote:
int minWidth = (ByteStyleX == ByteStyle.CommaSep) ? 5 : 4;
if (mIndex + minWidth > MaxOperandLen) {
Flush();
} else {
mBuffer[mIndex++] = Delimiter;
mBuffer[mIndex++] = ',';
}
break;
case State.OutQuote:
minWidth = (ByteStyleX == ByteStyle.CommaSep) ? 4 : 2;
if (mIndex + minWidth > MaxOperandLen) {
Flush();
} else {
if (ByteStyleX == ByteStyle.CommaSep) {
mBuffer[mIndex++] = ',';
}
}
break;
default:
Debug.Assert(false);
break;
}
if (ByteStyleX == ByteStyle.CommaSep) {
mBuffer[mIndex++] = '$';
}
mBuffer[mIndex++] = mHexChars[val >> 4];
mBuffer[mIndex++] = mHexChars[val & 0x0f];
mState = State.OutQuote;
}
/// <summary>
/// Tells the object to flush any pending data to the output.
/// </summary>
public void Finish() {
Flush();
}
/// <summary>
/// Outputs the buffer of pending data. A closing delimiter will be added if needed.
/// </summary>
private void Flush() {
switch (mState) {
case State.StartOfLine:
// empty string; put out a pair of delimiters
mBuffer[mIndex++] = Delimiter;
mBuffer[mIndex++] = Delimiter;
NumLinesOutput++;
break;
case State.InQuote:
// add delimiter and finish
mBuffer[mIndex++] = Delimiter;
NumLinesOutput++;
break;
case State.OutQuote:
// just output it
NumLinesOutput++;
break;
}
if (!IsTestRun) {
Gen.OutputLine(Label, Opcode, new string(mBuffer, 0, mIndex),
Comment);
}
mIndex = 0;
// Erase these after first use so we don't put them on every line.
Label = Comment = string.Empty;
}
}
}

View File

@ -35,11 +35,15 @@ namespace SourceGenWPF {
///
/// The ItemsControl.ItemsSource property wants an IEnumerable (which IList implements).
/// According to various articles, if the object implements IList, and the UI element
/// is providing UI virtualization, you will also get data virtualization. This behavior
/// is providing *UI* virtualization, you will also get *data* virtualization. This behavior
/// doesn't seem to be documented anywhere, but the consensus is that it's expected to work.
///
/// Implementing generic IList doesn't seem necessary for XAML, but is useful for other
/// customers of the data (e.g. the assembler source generator).
/// Implementing generic IList&lt;&gt; doesn't seem necessary for XAML, but may be useful
/// for other consumers of the data.
///
/// The list is initially filled with null references, with FormattedParts instances
/// generated on demand. This is done by requesting individual items from the
/// DisplayListGen object.
/// </remarks>
public class DisplayList : IList<DisplayList.FormattedParts>, IList,
INotifyCollectionChanged, INotifyPropertyChanged {
@ -47,11 +51,20 @@ namespace SourceGenWPF {
// TODO: check VirtualizingStackPanel.VirtualizationMode == recycling (page 259)
/// <summary>
/// List of formatted parts. The idea is that the list is initially populated with
/// null references, and FormattedParts objects are generated on demand.
/// List of formatted parts. DO NOT access this directly outside the event-sending
/// method wrappers.
/// </summary>
private List<FormattedParts> mList;
/// <summary>
/// Data generation object.
/// </summary>
/// <remarks>
/// This property is set by the LineListGen constructor.
/// </remarks>
public LineListGen ListGen { get; set; }
/// <summary>
/// Constructs an empty collection, with the default initial capacity.
/// </summary>
@ -59,14 +72,6 @@ namespace SourceGenWPF {
mList = new List<FormattedParts>();
}
public DisplayList(int count) {
mList = new List<FormattedParts>(count);
for (int i = 0; i < count; i++) {
mList.Add(null);
}
}
#region Property / Collection Changed
@ -279,12 +284,18 @@ namespace SourceGenWPF {
/// Retrieves the Nth element.
/// </summary>
private FormattedParts GetEntry(int index) {
Debug.WriteLine("GEN " + index);
if ((index % 10) != 0) {
return FormattedParts.Create("off" + index, "addr" + index, "12 34",
"vncidmx", "", "yup:", "LDA", "$1234", "a & b");
} else {
return FormattedParts.Create("yup: This is a long comment line");
FormattedParts parts = mList[index];
if (parts == null) {
parts = mList[index] = ListGen.GetFormattedParts(index);
}
return parts;
}
public void ResetList(int size) {
Clear();
mList.Capacity = size;
for (int i = 0; i < size; i++) {
Add(null);
}
}
@ -328,6 +339,33 @@ namespace SourceGenWPF {
return parts;
}
public static FormattedParts CreateBlankLine() {
FormattedParts parts = new FormattedParts();
return parts;
}
public static FormattedParts CreateLongComment(string comment) {
FormattedParts parts = new FormattedParts();
parts.Comment = comment;
return parts;
}
public static FormattedParts CreateDirective(string opstr, string addrStr) {
FormattedParts parts = new FormattedParts();
parts.Opcode = opstr;
parts.Operand = addrStr;
return parts;
}
public static FormattedParts CreateEquDirective(string label, string opstr,
string addrStr, string comment) {
FormattedParts parts = new FormattedParts();
parts.Label = label;
parts.Opcode = opstr;
parts.Operand = addrStr;
parts.Comment = comment;
return parts;
}
}
}
}

View File

@ -20,17 +20,28 @@ using System.Windows.Media;
using System.Text;
using Asm65;
using FormattedParts = SourceGenWPF.DisplayList.FormattedParts;
namespace SourceGenWPF {
/// <summary>
/// Converts file data and Anattrib contents into a series of strings and format metadata.
/// </summary>
public class DisplayListGen {
public class LineListGen {
/// <summary>
/// List of display lines.
/// </summary>
private List<Line> mLineList;
/// <summary>
/// List of formatted parts to be presented to the user. This has one entry per line.
/// </summary>
/// <remarks>
/// Separating FormattedParts out of Line seems odd at first, but we need changes to
/// DisplayList to cause events in XAML. I'm thinking the artificial separation of
/// Line from the formatted data holder may make future ports easier.
/// </remarks>
private DisplayList mDisplayList;
/// <summary>
/// Project that contains the data we're formatting, notably the FileData and
/// Anattribs arrays.
@ -53,68 +64,6 @@ namespace SourceGenWPF {
private PseudoOp.PseudoOpNames mPseudoOpNames;
/// <summary>
/// Holds a collection of formatted strings. Instances are immutable.
/// </summary>
public class FormattedParts {
public string Offset { get; private set; }
public string Addr { get; private set; }
public string Bytes { get; private set; }
public string Flags { get; private set; }
public string Attr { get; private set; }
public string Label { get; private set; }
public string Opcode { get; private set; }
public string Operand { get; private set; }
public string Comment { get; private set; }
// Use factory methods.
private FormattedParts() { }
public static FormattedParts Create(string offset, string addr, string bytes,
string flags, string attr, string label, string opcode, string operand,
string comment, string debug) {
FormattedParts parts = new FormattedParts();
parts.Offset = offset;
parts.Addr = addr;
parts.Bytes = bytes;
parts.Flags = flags;
parts.Attr = attr;
parts.Label = label;
parts.Opcode = opcode;
parts.Operand = operand;
parts.Comment = comment;
return parts;
}
public static FormattedParts CreateBlankLine() {
FormattedParts parts = new FormattedParts();
return parts;
}
public static FormattedParts CreateLongComment(string comment) {
FormattedParts parts = new FormattedParts();
parts.Comment = comment;
return parts;
}
public static FormattedParts CreateDirective(string opstr, string addrStr) {
FormattedParts parts = new FormattedParts();
parts.Opcode = opstr;
parts.Operand = addrStr;
return parts;
}
public static FormattedParts CreateEquDirective(string label, string opstr,
string addrStr, string comment) {
FormattedParts parts = new FormattedParts();
parts.Label = label;
parts.Opcode = opstr;
parts.Operand = addrStr;
parts.Comment = comment;
return parts;
}
}
/// <summary>
/// One of these per line of output in the display. It should be possible to draw
/// all of the output without needing to refer back to the project data. (Currently
@ -174,6 +123,11 @@ namespace SourceGenWPF {
/// Strings for display. Creation may be deferred. Use the DisplayList
/// GetFormattedParts() method to access this property.
/// </summary>
/// <remarks>
/// Certain elements, such as multi-line comments, must be formatted to determine
/// the number of lines they span. We retain the results to avoid formatting
/// them twice.
/// </remarks>
public FormattedParts Parts { get; set; }
/// <summary>
@ -277,7 +231,7 @@ namespace SourceGenWPF {
/// <param name="dl">Display list, with list of Lines.</param>
/// <param name="sel">Bit vector specifying which lines are selected.</param>
/// <returns>New SavedSelection object.</returns>
public static SavedSelection Generate(DisplayListGen dl, VirtualListViewSelection sel,
public static SavedSelection Generate(LineListGen dl, VirtualListViewSelection sel,
int topOffset) {
SavedSelection savedSel = new SavedSelection();
//Debug.Assert(topOffset >= 0);
@ -343,7 +297,7 @@ namespace SourceGenWPF {
/// </summary>
/// <param name="dl">Display list, with list of Lines.</param>
/// <returns>Set of selected lines.</returns>
public VirtualListViewSelection Restore(DisplayListGen dl, out int topIndex) {
public VirtualListViewSelection Restore(LineListGen dl, out int topIndex) {
List<Line> lineList = dl.mLineList;
VirtualListViewSelection sel = new VirtualListViewSelection(lineList.Count);
@ -414,15 +368,23 @@ namespace SourceGenWPF {
/// </summary>
/// <param name="proj">Project object.</param>
/// <param name="formatter">Formatter object.</param>
public DisplayListGen(DisasmProject proj, Formatter formatter,
public LineListGen(DisasmProject proj, DisplayList displayList, Formatter formatter,
PseudoOp.PseudoOpNames opNames) {
Debug.Assert(proj != null);
Debug.Assert(displayList != null);
Debug.Assert(formatter != null);
Debug.Assert(opNames != null);
mProject = proj;
mDisplayList = displayList;
mFormatter = formatter;
mPseudoOpNames = opNames;
mLineList = new List<Line>();
mShowCycleCounts = AppSettings.Global.GetBool(AppSettings.SRCGEN_SHOW_CYCLE_COUNTS,
false);
mDisplayList.ListGen = this;
}
/// <summary>
@ -432,6 +394,7 @@ namespace SourceGenWPF {
public void SetFormatter(Formatter formatter) {
mFormatter = formatter;
mLineList.Clear();
// TODO: update display list
// We probably just changed settings, so update this as well.
mShowCycleCounts = AppSettings.Global.GetBool(AppSettings.SRCGEN_SHOW_CYCLE_COUNTS,
@ -446,6 +409,7 @@ namespace SourceGenWPF {
public void SetPseudoOpNames(PseudoOp.PseudoOpNames opNames) {
mPseudoOpNames = opNames;
mLineList.Clear();
// TODO: update display list
}
/// <summary>
@ -490,8 +454,7 @@ namespace SourceGenWPF {
// should have been done already
default:
Debug.Assert(false);
parts = FormattedParts.Create("x", "x", "x", "x", "x", "x", "x", "x",
"x", "x");
parts = FormattedParts.Create("x", "x", "x", "x", "x", "x", "x", "x", "x");
break;
}
line.Parts = parts;
@ -617,6 +580,8 @@ namespace SourceGenWPF {
GenerateLineList(mProject, mFormatter, mPseudoOpNames,
0, mProject.FileData.Length - 1, mLineList);
mDisplayList.ResetList(mLineList.Count);
Debug.Assert(ValidateLineList(), "Display list failed validation");
}
@ -689,6 +654,7 @@ namespace SourceGenWPF {
// Out with the old, in with the new.
mLineList.RemoveRange(startIndex, endIndex - startIndex + 1);
mLineList.InsertRange(startIndex, newLines);
// TODO: update display list
Debug.Assert(ValidateLineList(), "Display list failed validation");
}
@ -754,6 +720,7 @@ namespace SourceGenWPF {
}
Debug.WriteLine("Removing " + endIndex + " header lines");
mLineList.RemoveRange(0, endIndex);
// TODO: update display list
}
/// <summary>
@ -1169,12 +1136,8 @@ namespace SourceGenWPF {
}
string commentStr = formatter.FormatEolComment(eolComment);
string debugStr = string.Empty;
//debugStr = "opOff=" +
// (attr.OperandOffset < 0 ? "-" : "+" + attr.OperandOffset.ToString("x6"));
FormattedParts parts = FormattedParts.Create(offsetStr, addrStr, bytesStr,
flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr, debugStr);
flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr);
return parts;
}
@ -1184,9 +1147,9 @@ namespace SourceGenWPF {
byte[] data = proj.FileData;
string offsetStr, addrStr, bytesStr, flagsStr, attrStr, labelStr, opcodeStr,
operandStr, commentStr, debugStr;
operandStr, commentStr;
offsetStr = addrStr = bytesStr = flagsStr = attrStr = labelStr = opcodeStr =
operandStr = commentStr = debugStr = string.Empty;
operandStr = commentStr = string.Empty;
PseudoOp.PseudoOut pout = PseudoOp.FormatDataOp(formatter, opNames, proj.SymbolTable,
null, attr.DataDescriptor, proj.FileData, offset, subLineIndex);
@ -1210,13 +1173,10 @@ namespace SourceGenWPF {
if (subLineIndex == 0) {
commentStr = formatter.FormatEolComment(proj.Comments[offset]);
//debugStr = "opOff=" +
// (attr.OperandOffset < 0 ? "-" : "+" + attr.OperandOffset.ToString("x6"));
}
FormattedParts parts = FormattedParts.Create(offsetStr, addrStr, bytesStr,
flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr, debugStr);
flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr);
return parts;
}
}

View File

@ -51,7 +51,6 @@ namespace SourceGenWPF {
/// an empty symbol table.
/// </summary>
private SymbolTableSubset mSymbolSubset;
#endif
/// <summary>
/// Current code list view selection. The length will match the DisplayList Count.
@ -61,11 +60,12 @@ namespace SourceGenWPF {
/// notifies us of changes to the selection, so we can track it ourselves.
/// </summary>
private VirtualListViewSelection mCodeViewSelection = new VirtualListViewSelection();
#endif
/// <summary>
/// Data backing the codeListView.
/// Data backing the code list.
/// </summary>
private DisplayListGen mDisplayList;
public LineListGen CodeListGen { get; private set; }
#endregion Project state
@ -151,6 +151,171 @@ namespace SourceGenWPF {
mMainWin = win;
}
/// <summary>
/// Perform one-time initialization after the Window has finished loading. We defer
/// to this point so we can report fatal errors directly to the user.
/// </summary>
public void WindowLoaded() {
if (RuntimeDataAccess.GetDirectory() == null) {
MessageBox.Show(Res.Strings.RUNTIME_DIR_NOT_FOUND,
Res.Strings.RUNTIME_DIR_NOT_FOUND_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Error);
Application.Current.Shutdown();
return;
}
#if false
try {
PluginDllCache.PreparePluginDir();
} catch (Exception ex) {
string pluginPath = PluginDllCache.GetPluginDirPath();
if (pluginPath == null) {
pluginPath = "<???>";
}
string msg = string.Format(Properties.Resources.PLUGIN_DIR_FAIL,
pluginPath + ": " + ex.Message);
MessageBox.Show(this, msg, Properties.Resources.PLUGIN_DIR_FAIL_CAPTION,
MessageBoxButtons.OK, MessageBoxIcon.Error);
Application.Exit();
return;
}
#endif
#if false
logoPictureBox.ImageLocation = RuntimeDataAccess.GetPathName(LOGO_FILE_NAME);
versionLabel.Text = string.Format(Properties.Resources.VERSION_FMT,
Program.ProgramVersion);
toolStripStatusLabel.Text = Properties.Resources.STATUS_READY;
mProjectControl = this.codeListView;
mNoProjectControl = this.noProjectPanel;
// Clone the menu structure from the designer. The same items are used for
// both Edit > Actions and the right-click context menu in codeListView.
mActionsMenuItems = new ToolStripItem[actionsToolStripMenuItem.DropDownItems.Count];
for (int i = 0; i < actionsToolStripMenuItem.DropDownItems.Count; i++) {
mActionsMenuItems[i] = actionsToolStripMenuItem.DropDownItems[i];
}
#endif
#if false
// Load the settings from the file. Some things (like the symbol subset) need
// these. The general "apply settings" doesn't happen until a bit later, after
// the sub-windows have been initialized.
LoadAppSettings();
// Init primary ListView (virtual, ownerdraw)
InitCodeListView();
// Init Symbols ListView (virtual, non-ownerdraw)
mSymbolSubset = new SymbolTableSubset(new SymbolTable());
symbolListView.SetDoubleBuffered(true);
InitSymbolListView();
// Init References ListView (non-virtual, non-ownerdraw)
referencesListView.SetDoubleBuffered(true);
// Place the main window and apply the various settings.
SetAppWindowLocation();
#endif
ApplyAppSettings();
#if false
UpdateActionMenu();
UpdateMenuItemsAndTitle();
UpdateRecentLinks();
ShowNoProject();
#endif
ProcessCommandLine();
}
private void ProcessCommandLine() {
string[] args = Environment.GetCommandLineArgs();
if (args.Length == 2) {
DoOpenFile(Path.GetFullPath(args[1]));
}
}
/// <summary>
/// Applies "actionable" settings to the ProjectView, pulling them out of the global
/// settings object. If a project is open, refreshes the display list and all sub-windows.
/// </summary>
private void ApplyAppSettings() {
Debug.WriteLine("ApplyAppSettings...");
AppSettings settings = AppSettings.Global;
// Set up the formatter.
mFormatterConfig = new Formatter.FormatConfig();
AsmGen.GenCommon.ConfigureFormatterFromSettings(AppSettings.Global,
ref mFormatterConfig);
mFormatterConfig.mEndOfLineCommentDelimiter = ";";
mFormatterConfig.mFullLineCommentDelimiterBase = ";";
mFormatterConfig.mBoxLineCommentDelimiter = string.Empty;
mFormatterConfig.mAllowHighAsciiCharConst = true;
mOutputFormatter = new Formatter(mFormatterConfig);
mOutputFormatterCpuDef = null;
// Set pseudo-op names. Entries aren't allowed to be blank, so we start with the
// default values and merge in whatever the user has configured.
mPseudoOpNames = PseudoOp.sDefaultPseudoOpNames.GetCopy();
string pseudoCereal = settings.GetString(AppSettings.FMT_PSEUDO_OP_NAMES, null);
if (!string.IsNullOrEmpty(pseudoCereal)) {
PseudoOp.PseudoOpNames deser = PseudoOp.PseudoOpNames.Deserialize(pseudoCereal);
if (deser != null) {
mPseudoOpNames.Merge(deser);
}
}
#if false
// Configure the Symbols window.
symbolUserCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_USER, false);
symbolAutoCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_AUTO, false);
symbolProjectCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_PROJECT, false);
symbolPlatformCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_PLATFORM, false);
symbolConstantCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_CONST, false);
symbolAddressCheckBox.Checked =
settings.GetBool(AppSettings.SYMWIN_SHOW_ADDR, false);
// Set the code list view font.
string fontStr = settings.GetString(AppSettings.CDLV_FONT, null);
if (!string.IsNullOrEmpty(fontStr)) {
FontConverter cvt = new FontConverter();
try {
Font font = cvt.ConvertFromInvariantString(fontStr) as Font;
codeListView.Font = font;
Debug.WriteLine("Set font to " + font.ToString());
} catch (Exception ex) {
Debug.WriteLine("Font convert failed: " + ex.Message);
}
}
// Unpack the recent-project list.
UnpackRecentProjectList();
// Enable the DEBUG menu if configured.
bool showDebugMenu = AppSettings.Global.GetBool(AppSettings.DEBUG_MENU_ENABLED, false);
if (dEBUGToolStripMenuItem.Visible != showDebugMenu) {
dEBUGToolStripMenuItem.Visible = showDebugMenu;
mainMenuStrip.Refresh();
}
#endif
// Finally, update the display list generator with all the fancy settings.
if (CodeListGen != null) {
// Regenerate the display list with the latest formatter config and
// pseudo-op definition. (These are set as part of the refresh.)
UndoableChange uc =
UndoableChange.CreateDummyChange(UndoableChange.ReanalysisScope.DisplayOnly);
ApplyChanges(new ChangeSet(uc), false);
}
}
/// <summary>
/// Ensures that the named project is at the top of the list. If it's elsewhere
@ -519,7 +684,7 @@ namespace SourceGenWPF {
proj.Initialize(fileData.Length);
proj.PrepForNew(fileData, Path.GetFileName(dataPathName));
proj.LongComments.Add(DisplayListGen.Line.HEADER_COMMENT_OFFSET,
proj.LongComments.Add(LineListGen.Line.HEADER_COMMENT_OFFSET,
new MultiLineComment("6502bench SourceGen v" + App.ProgramVersion));
// The system definition provides a set of defaults that can be overridden.
@ -540,7 +705,8 @@ namespace SourceGenWPF {
dlg.ShowDialog();
}
mDisplayList = new DisplayListGen(mProject, mOutputFormatter, mPseudoOpNames);
CodeListGen = new LineListGen(mProject, mMainWin.CodeDisplayList,
mOutputFormatter, mPseudoOpNames);
// Prep the symbol table subset object. Replace the old one with a new one.
//mSymbolSubset = new SymbolTableSubset(mProject.SymbolTable);
@ -624,9 +790,9 @@ namespace SourceGenWPF {
#else
int topItem = 0;
#endif
int topOffset = mDisplayList[topItem].FileOffset;
DisplayListGen.SavedSelection savedSel = DisplayListGen.SavedSelection.Generate(
mDisplayList, mCodeViewSelection, topOffset);
int topOffset = CodeListGen[topItem].FileOffset;
LineListGen.SavedSelection savedSel = LineListGen.SavedSelection.Generate(
CodeListGen, null /*mCodeViewSelection*/, topOffset);
//savedSel.DebugDump();
mReanalysisTimer.EndTask("Save selection");
@ -647,7 +813,7 @@ namespace SourceGenWPF {
}
mReanalysisTimer.EndTask(refreshTaskStr);
VirtualListViewSelection newSel = savedSel.Restore(mDisplayList, out int topIndex);
VirtualListViewSelection newSel = savedSel.Restore(CodeListGen, out int topIndex);
//newSel.DebugDump();
// Refresh the various windows, and restore the selection.
@ -708,8 +874,8 @@ namespace SourceGenWPF {
Debug.WriteLine("CpuDef has changed, resetting formatter (now " +
mProject.CpuDef + ")");
mOutputFormatter = new Formatter(mFormatterConfig);
mDisplayList.SetFormatter(mOutputFormatter);
mDisplayList.SetPseudoOpNames(mPseudoOpNames);
CodeListGen.SetFormatter(mOutputFormatter);
CodeListGen.SetPseudoOpNames(mPseudoOpNames);
mOutputFormatterCpuDef = mProject.CpuDef;
}
@ -733,8 +899,10 @@ namespace SourceGenWPF {
toolStripStatusLabel.Text = prevStatus;
}
} else {
#endif
DoRefreshProject(reanalysisRequired);
}
#if false
}
#endif
if (FormatDescriptor.DebugCreateCount != 0) {
@ -755,7 +923,7 @@ namespace SourceGenWPF {
IEnumerator<RangeSet.Range> iter = offsetSet.RangeListIterator;
while (iter.MoveNext()) {
RangeSet.Range range = iter.Current;
mDisplayList.GenerateRange(range.Low, range.High);
CodeListGen.GenerateRange(range.Low, range.High);
}
}
@ -783,7 +951,7 @@ namespace SourceGenWPF {
}
mReanalysisTimer.StartTask("Generate DisplayList");
mDisplayList.GenerateAll();
CodeListGen.GenerateAll();
mReanalysisTimer.EndTask("Generate DisplayList");
}

View File

@ -22,7 +22,8 @@ limitations under the License.
mc:Ignorable="d"
Title="6502bench SourceGen"
Icon="/SourceGenWPF;component/Res/SourceGenIcon.ico"
Width="810" Height="510" MinWidth="800" MinHeight="500">
Width="810" Height="510" MinWidth="800" MinHeight="500"
Loaded="Window_Loaded">
<Window.Resources>
<ResourceDictionary>
@ -31,13 +32,6 @@ limitations under the License.
<ResourceDictionary Source="CodeListItemStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
<RoutedUICommand x:Key="AssembleCmd" Text="Assemble...">
<RoutedUICommand.InputGestures>
<KeyGesture>Ctrl+Shift+A</KeyGesture>
</RoutedUICommand.InputGestures>
</RoutedUICommand>
<RoutedUICommand x:Key="RecentProject"/>
<!-- don't center the ListView(GridView) column headers
https://stackoverflow.com/q/44119146/294248
(style without ID applies to all instances of that type)
@ -45,6 +39,13 @@ limitations under the License.
<Style TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="HorizontalContentAlignment" Value="Left" />
</Style>
<RoutedUICommand x:Key="AssembleCmd" Text="Assemble...">
<RoutedUICommand.InputGestures>
<KeyGesture>Ctrl+Shift+A</KeyGesture>
</RoutedUICommand.InputGestures>
</RoutedUICommand>
<RoutedUICommand x:Key="RecentProject"/>
</ResourceDictionary>
</Window.Resources>
@ -54,7 +55,7 @@ limitations under the License.
</Window.CommandBindings>
<DockPanel>
<Menu Name="AppMenu" DockPanel.Dock="Top">
<Menu Name="appMenu" DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="New"/>
<MenuItem Header="Open"/>
@ -136,13 +137,13 @@ limitations under the License.
<Button Content="tbb" Width="75"/>
</ToolBar>
<StatusBar Name="MainStatusBar" DockPanel.Dock="Bottom">
<StatusBar Name="mainStatusBar" DockPanel.Dock="Bottom">
<TextBlock Text="Ready"/>
</StatusBar>
<!-- Main part of the window. Three side-by-side panels, only the middle of which changes
size when the window is resized. -->
<Grid Name="TriptychGrid" DockPanel.Dock="Top">
<Grid Name="triptychGrid" DockPanel.Dock="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="168" MinWidth="100"/>
<ColumnDefinition Width="Auto"/>
@ -155,7 +156,7 @@ limitations under the License.
<GridSplitter Width="4" Grid.Column="1" HorizontalAlignment="Left"/>
<GridSplitter Width="4" Grid.Column="3" HorizontalAlignment="Center"/>
<Grid Name="LeftPanel" Grid.Column="0">
<Grid Name="leftPanel" Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="*" MinHeight="100"/>
<RowDefinition Height="Auto"/>
@ -163,7 +164,7 @@ limitations under the License.
</Grid.RowDefinitions>
<GroupBox Grid.Row="0" Header="References">
<DataGrid Name="ReferencesList" IsReadOnly="True"
<DataGrid Name="referencesList" IsReadOnly="True"
FontFamily="{StaticResource GeneralMonoFont}">
<DataGrid.Columns>
<DataGridTextColumn Header="Offset" Binding="{Binding Offset}"/>
@ -174,7 +175,7 @@ limitations under the License.
</GroupBox>
<GroupBox Grid.Row="2" Header="Notes">
<DataGrid Name="NotesList" IsReadOnly="True"
<DataGrid Name="notesList" IsReadOnly="True"
FontFamily="{StaticResource GeneralMonoFont}">
<DataGrid.Columns>
<DataGridTextColumn Header="Offset" Binding="{Binding Offset}"/>
@ -245,7 +246,7 @@ limitations under the License.
</ListView>
<Grid Name="RightPanel" Grid.Column="4">
<Grid Name="rightPanel" Grid.Column="4">
<Grid.RowDefinitions>
<RowDefinition Height="*" MinHeight="100"/>
<RowDefinition Height="Auto"/>

View File

@ -36,23 +36,41 @@ namespace SourceGenWPF.ProjWin {
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged {
/// <summary>
private MainController mUI;
/// Disassembled code display list provided to XAML.
/// </summary>
public DisplayList CodeDisplayList { get; private set; }
/// <summary>
/// </summary>
private MainController mMainCtrl;
public MainWindow() {
InitializeComponent();
// TODO: verify that RuntimeData dir is accessible
this.DataContext = this;
mUI = new MainController(this);
codeListView.ItemsSource = new DisplayList(500);
CodeDisplayList = new DisplayList();
codeListView.ItemsSource = CodeDisplayList;
GridView gv = (GridView)codeListView.View;
mMainCtrl = new MainController(this);
//GridView gv = (GridView)codeListView.View;
//gv.Columns[0].Width = 50;
}
private void Window_Loaded(object sender, RoutedEventArgs e) {
mMainCtrl.WindowLoaded();
#if DEBUG
// Get more info on CollectionChanged events that do not agree with current
// state of Items collection.
PresentationTraceSources.SetTraceLevel(codeListView.ItemContainerGenerator,
PresentationTraceLevel.High);
}
#endif
/// <summary>
/// INotifyPropertyChanged event
/// </summary>
@ -119,7 +137,7 @@ namespace SourceGenWPF.ProjWin {
recentIndex--;
Debug.WriteLine("Recent project #" + recentIndex);
mUI.OpenRecentProject(recentIndex);
mMainCtrl.OpenRecentProject(recentIndex);
}
private void CodeListView_SelectionChanged(object sender, SelectionChangedEventArgs e) {

View File

@ -721,7 +721,7 @@ namespace SourceGenWPF {
// Shouldn't allow DisplayList.Line.HEADER_COMMENT_OFFSET on anything but
// LongComment. Maybe "bool allowNegativeKeys"?
if (intKey < fileLen &&
(intKey >= 0 || intKey == DisplayListGen.Line.HEADER_COMMENT_OFFSET)) {
(intKey >= 0 || intKey == LineListGen.Line.HEADER_COMMENT_OFFSET)) {
return true;
} else {
report.Add(FileLoadItem.Type.Warning,

View File

@ -26,6 +26,7 @@
<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_GeneratedForVersion">Target assembler: {0} v{1} [{2}]</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>
@ -39,6 +40,8 @@
<system:String x:Key="str_OpenDataWrongLengthFmt">The file is {0:N0} bytes long, but the project expected {1:N0}.</system:String>
<system:String x:Key="str_OpenDataWrongCrcFmt">The file has CRC {0}, but the project expected {1}.</system:String>
<system:String x:Key="str_OperationFailed">Failed</system:String>
<system:String x:Key="str_ProgressAssembling">Executing assembler...</system:String>
<system:String x:Key="str_ProgressGeneratingFmt">Generating {0}...</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>
@ -47,5 +50,7 @@
<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_RuntimeDirNotFound">The RuntimeData directory was not found. It should be in the same directory as the executable.</system:String>
<system:String x:Key="str_RuntimeDirNotFoundCaption">RuntimeData Not Found</system:String>
<system:String x:Key="str_SetupSystemSummaryFmt">{1} CPU @ {2} MHz</system:String>
</ResourceDictionary>

View File

@ -64,6 +64,8 @@ namespace SourceGenWPF.Res {
(string)Application.Current.FindResource("str_FileFilterDis65");
public static string FILE_FILTER_SYM65 =
(string)Application.Current.FindResource("str_FileFilterSym65");
public static string GENERATED_FOR_VERSION_FMT =
(string)Application.Current.FindResource("str_GeneratedForVersion");
public static string INITIAL_EXTENSION_SCRIPTS =
(string)Application.Current.FindResource("str_InitialExtensionScripts");
public static string INITIAL_PARAMETERS =
@ -90,6 +92,10 @@ namespace SourceGenWPF.Res {
(string)Application.Current.FindResource("str_OpenDataWrongLengthFmt");
public static string OPERATION_FAILED =
(string)Application.Current.FindResource("str_OperationFailed");
public static string PROGRESS_ASSEMBLING =
(string)Application.Current.FindResource("str_ProgressAssembling");
public static string PROGRESS_GENERATING_FMT =
(string)Application.Current.FindResource("str_ProgressGeneratingFmt");
public static string PROJECT_FIELD_COMMENT =
(string)Application.Current.FindResource("str_ProjectFieldComment");
public static string PROJECT_FIELD_LONG_COMMENT =
@ -106,6 +112,10 @@ namespace SourceGenWPF.Res {
(string)Application.Current.FindResource("str_ProjectFieldUserLabel");
public static string PROJECT_FROM_NEWER_APP =
(string)Application.Current.FindResource("str_ProjectFromNewerApp");
public static string RUNTIME_DIR_NOT_FOUND =
(string)Application.Current.FindResource("str_RuntimeDirNotFound");
public static string RUNTIME_DIR_NOT_FOUND_CAPTION =
(string)Application.Current.FindResource("str_RuntimeDirNotFoundCaption");
public static string SETUP_SYSTEM_SUMMARY_FMT =
(string)Application.Current.FindResource("str_SetupSystemSummaryFmt");
}

View File

@ -62,6 +62,17 @@
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="AsmGen\AsmCc65.cs" />
<Compile Include="AsmGen\AsmMerlin32.cs" />
<Compile Include="AsmGen\AsmTass64.cs" />
<Compile Include="AsmGen\AssemblerConfig.cs" />
<Compile Include="AsmGen\AssemblerInfo.cs" />
<Compile Include="AsmGen\AssemblerVersion.cs" />
<Compile Include="AsmGen\GenCommon.cs" />
<Compile Include="AsmGen\IAssembler.cs" />
<Compile Include="AsmGen\IGenerator.cs" />
<Compile Include="AsmGen\LabelLocalizer.cs" />
<Compile Include="AsmGen\StringGather.cs" />
<Compile Include="DisplayList.cs" />
<Compile Include="MainController.cs" />
<Compile Include="ProjWin\DataFileLoadIssue.xaml.cs">
@ -97,7 +108,7 @@
<Compile Include="DataAnalysis.cs" />
<Compile Include="DefSymbol.cs" />
<Compile Include="DisasmProject.cs" />
<Compile Include="DisplayListGen.cs" />
<Compile Include="LineListGen.cs" />
<Compile Include="ExternalFile.cs" />
<Compile Include="FormatDescriptor.cs" />
<Compile Include="HelpAccess.cs" />