Created
September 20, 2025 11:19
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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