Skip to content

Instantly share code, notes, and snippets.

@nzeemin
Created September 20, 2025 11:19
Show Gist options
  • Save nzeemin/c0d44bc89c0b16fd4d69932bb32c3b1c to your computer and use it in GitHub Desktop.
Save nzeemin/c0d44bc89c0b16fd4d69932bb32c3b1c to your computer and use it in GitHub Desktop.
ParseMacroListing C# program used to analyse .LST file from MACRO11 and find possible issues in the code
using System.Text;
using System.Text.RegularExpressions;
namespace ParseMacroListing;
public class LineInfo
{
public int Number { get; init; }
public ushort Address { get; init; }
public int NumValues { get; set; }
public ushort Value1 { get; set; }
public ushort Value2 { get; set; }
public ushort Value3 { get; set; }
public string? Line { get; init; }
public string Source { get; init; }
public bool IsData { get; set; }
}
class Program
{
private static List<LineInfo> _lines = new List<LineInfo>();
private static int _totalIssuesFound = 0;
[STAThread]
static int Main(string[] args)
{
string? fileName = null;
if (args.Length > 0)
fileName = args[0];
if (args.Length < 1)
{
var files = Directory.EnumerateFiles(".", "*.LST", SearchOption.TopDirectoryOnly);
fileName = files.FirstOrDefault();
}
if (fileName == null)
{
Console.WriteLine("Filename argument not specified, can't find an *.LST file in the current directory.");
return -1;
}
Console.WriteLine($"Parsing file {fileName}");
using (var fileStream = File.OpenRead(fileName))
using (var streamReader = new StreamReader(fileStream, Encoding.UTF8, true))
{
ParseFile(streamReader);
}
//Console.WriteLine("Processing lines");
ProcessLines();
if (_totalIssuesFound == 0)
Console.WriteLine("DONE, no issues found.");
else
Console.WriteLine($"DONE, {_totalIssuesFound} issues found.");
return 0;
}
static void ParseFile(StreamReader streamReader)
{
string? line;
while ((line = streamReader.ReadLine()) != null)
{
if (line.Length == 0)
continue;
// Address only
var regexA = new Regex(@"^[ ]+(\d+)\t([0-7]+)\t\t\t\t(.*)$");
var matchA = regexA.Match(line);
if (matchA.Success)
{
var lineInfo = new LineInfo
{
Number = int.Parse(matchA.Groups[1].Value),
Address = (ushort)Convert.ToInt32(matchA.Groups[2].Value.TrimEnd(), 8),
NumValues = 0,
Line = matchA.Groups[3].Value,
Source = line,
IsData = CheckIsDataLine(matchA.Groups[3].Value)
};
_lines.Add(lineInfo);
continue;
}
//TODO: Address and 1..3 byte values
// Address and 1..3 word values
var regex1 = new Regex(@"^[ ]+(\d+)\t([0-7]+)\t([0-7]+[ ]?)\t([0-7]+[ ]?)?\t([0-7]+[ ]?)?\t(.*)$");
var match1 = regex1.Match(line);
if (match1.Success)
{
var lineInfo = new LineInfo
{
Number = int.Parse(match1.Groups[1].Value),
Address = (ushort)Convert.ToInt32(match1.Groups[2].Value.TrimEnd(), 8),
NumValues = 1,
Value1 = (ushort)Convert.ToInt32(match1.Groups[3].Value.TrimEnd(), 8),
Line = match1.Groups[6].Value,
Source = line,
IsData = CheckIsDataLine(match1.Groups[6].Value)
};
if (!string.IsNullOrWhiteSpace(match1.Groups[4].Value))
{
lineInfo.Value2 = (ushort)Convert.ToInt32(match1.Groups[4].Value.TrimEnd(), 8);
lineInfo.NumValues = 2;
}
if (!string.IsNullOrWhiteSpace(match1.Groups[5].Value))
{
lineInfo.Value2 = (ushort)Convert.ToInt32(match1.Groups[5].Value.TrimEnd(), 8);
lineInfo.NumValues = 3;
}
_lines.Add(lineInfo);
continue;
}
}
}
static bool CheckIsDataLine(string line)
{
var regex = new Regex(@"^(([\w_]+\:)|\s+)\s*([\.\w]+)?");
var match = regex.Match(line);
if (!match.Success)
return false;
var statement = match.Groups[3].Value.Trim().ToUpper();
if (statement == ".WORD" || statement == ".BYTE" || statement == ".BLKW" || statement == ".BLKB")
return true;
return false;
}
static void ProcessLines()
{
if (_lines.Count == 0)
throw new InvalidOperationException("No lines have been parsed.");
for (var index = 0; index < _lines.Count; index++)
{
ProcessOneLine(index);
}
}
private static void ProcessOneLine(int index)
{
var line = _lines[index];
var prevLine = index > 0 ? _lines[index - 1] : new LineInfo();
var opcode = line.Value1;
var prevOpcode = prevLine.Value1;
if (line.IsData)
return;
if (IsOpcodeRETURN(opcode)) // RETURN
{
if (prevOpcode == 0x09F7) // 004767 CALL
{
RecordIssue("CALL / RETURN, could be replaced with JMP", prevLine, line);
return;
}
}
else if (IsOpcodeJMP(opcode)) // JMP
{
var destAddress = (ushort)(line.Address + line.Value2 + 4);
var offset = destAddress - line.Address;
// Short JMP
if (IsValidOffsetForBR(offset))
{
RecordIssue("JMP on short distance, could be replaced with BR", line);
return;
}
// Destination line to analyze "points to" rules
var destLine = FindLineByAddress(destAddress);
if (destLine != null)
{
var destOpcode = destLine.Value1;
// JMP points to BR
if (IsOpcodeBR(destOpcode)) // BR
{
//TODO: Check if the offset suitable for BR
RecordIssue("JMP points to BR, could be replaced with direct JMP", line, destLine);
return;
}
// JMP points to JMP
if (IsOpcodeJMP(destOpcode)) // JMP
{
RecordIssue("JMP points to JMP, could be replaced with direct JMP", line, destLine);
return;
}
// JMP points to RETURN
if (IsOpcodeRETURN(destOpcode)) // 000207 RETURN
{
RecordIssue("JMP points to RETURN, could be replaced with RETURN", line, destLine);
return;
}
}
}
else if (IsOpcodeBR(opcode)) // BR
{
// Destination address to analyze "points to" rules
var destAddress = (ushort)(line.Address + GetOffsetForBROpcode(opcode));
var destLine = FindLineByAddress(destAddress);
if (destLine != null)
{
var destOpcode = destLine.Value1;
// Условный переход (BEQ/BNE/итп) с обходом одного BR - заменяется на инверсию условия
if (IsOpcodeCondition(prevOpcode) && (prevOpcode & 0x00FF) == 0x0001)
{
RecordIssue("Condition Bxx to step over BR, could be replaced with inverted condition", prevLine, line);
return;
}
// BR points to BR
if (IsOpcodeBR(destOpcode)) // BR
{
var destAddress2 = (ushort)(destLine.Address + GetOffsetForBROpcode(destOpcode));
var destOffset = destAddress2 - line.Address;
if (IsValidOffsetForBR(destOffset))
{
RecordIssue("BR points to BR, could be replaced with direct BR", line, destLine);
}
return;
}
// BR points to JMP
if (IsOpcodeBR(destOpcode))
{
var destAddress2 = (ushort)(destLine.Address + destLine.Value2 + 4);
var destOffset = destAddress2 - line.Address;
if (IsValidOffsetForBR(destOffset))
{
RecordIssue("BR points to JMP, could be replaced with direct BR", line, destLine);
}
return;
}
// BR points to RETURN
if (IsOpcodeRETURN(destOpcode)) // 000207 RETURN
{
RecordIssue("BR points to RETURN, could be replaced with RETURN", line, destLine);
return;
}
}
}
else if (IsOpcodeCondition(opcode)) // BEQ/BNE/etc.
{
var offset = GetOffsetForBROpcode(opcode);
if (offset == 0)
{
RecordIssue("Condition Bxx points to itself (dead cycle)", line);
return;
}
if (offset == 2)
{
RecordIssue("Condition Bxx points right after it (useless)", line);
return;
}
// INC Rx / BNE назад
if ((opcode & 0xFF00) == 0x0200 && (prevOpcode & 0xFFF8) == 0x0AC7 &&
offset < 0 && offset >= -128) // BNE, prev is INC Rx
{
RecordIssue("INC Rx + BNE could be replaced with SOB Rx", prevLine, line);
return;
}
}
// MOV/MOVB #000000, R0
if ((opcode & 0x07FF8) == 0x15C0 && // 01127x MOV #xxx,Rx or 11127x MOVB #xxx,Rx
line.Value2 == 0)
{
RecordIssue("MOV/MOVB #0,Rx, could be replaced with CLR Rx", line);
return;
}
// Находить MOVB addr, Rx / BIC #177400, Rx
if ((prevOpcode & 0xF038) == 0x9000 && // MOVB xx,Rx
(opcode & 0xFFF8) == 0xC5C0 && line.Value2 == 0xFF00) // 04270x BIC #177400,Rx
{
RecordIssue("MOVB addr,Rx / BIC #177400,Rx, could be replaced with CLR Rx / BIS addr,Rx", prevLine, line);
return;
}
}
static bool IsOpcodeBR(ushort opcode) => ((opcode & 0xFF00) == 0x0100);
static int GetOffsetForBROpcode(ushort opcode) =>
((opcode & 0xFF) < 0x80 ? (opcode & 0xFF) * 2 : -((0x100 - (opcode & 0xFF)) * 2)) + 2;
static bool IsValidOffsetForBR(int offset) => (offset + 2 <= 0x0100 && offset + 2 >= -0x00FE);
static bool IsOpcodeJMP(ushort opcode) => opcode == 0x0077; // 000167 JMP
static bool IsOpcodeRETURN(ushort opcode) => opcode == 0x007B; // 000207 RETURN
static bool IsOpcodeCondition(ushort opcode)
{
var opcodehi = opcode & 0xFF00;
return opcodehi == 0x0200/*BNE*/ || opcodehi == 0x0300/*BEQ*/ || opcodehi == 0x8000/*BPL*/ || opcodehi == 0x8100/*BMI*/ ||
opcodehi == 0x8400/*BVC*/ || opcodehi == 0x8500/*BVS*/ || opcodehi == 0x8600/*BCC/BHIS*/ || opcodehi == 0x8700/*BCS/BLO*/ ||
opcodehi == 0x0400/*BGE*/ || opcodehi == 0x0500/*BLT*/ || opcodehi == 0x0600/*BGT*/ || opcodehi == 0x0700/*BLE*/ ||
opcodehi == 0x8200/*BHI*/ || opcodehi == 0x8300/*BLOS*/;
}
static void RecordIssue(string description, LineInfo line, LineInfo? line2 = null)
{
if (description == null) throw new ArgumentNullException(nameof(description));
if (line == null) throw new ArgumentNullException(nameof(line));
_totalIssuesFound++;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($"{description}:");
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine(line.Source);
if (line2 != null)
Console.WriteLine(line2.Source);
}
static LineInfo? FindLineByAddress(ushort address)
{
foreach (var line in _lines)
{
if (line.Address == address)
return line;
}
return null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment