From 7a8d2bcd75d88a5429937bf551878c55e1dcf0fd Mon Sep 17 00:00:00 2001 From: Melodi <62381002+Melodi17@users.noreply.github.com> Date: Wed, 6 Aug 2025 22:36:34 +1000 Subject: moved everything to seperate projects, and started compiler work + push and pop instructions added --- Assembler/Assembler.cs | 328 +++++++++++++++++++++++++-------------------- Assembler/Assembler.csproj | 19 +++ Assembler/Disassembler.cs | 74 ---------- Assembler/Extensions.cs | 15 +++ Assembler/GlobalUsings.cs | 12 ++ Assembler/Options.cs | 12 ++ Assembler/Program.cs | 38 ++++++ 7 files changed, 279 insertions(+), 219 deletions(-) create mode 100644 Assembler/Assembler.csproj delete mode 100644 Assembler/Disassembler.cs create mode 100644 Assembler/Extensions.cs create mode 100644 Assembler/GlobalUsings.cs create mode 100644 Assembler/Options.cs create mode 100644 Assembler/Program.cs (limited to 'Assembler') diff --git a/Assembler/Assembler.cs b/Assembler/Assembler.cs index 09ac30e..7c172e7 100644 --- a/Assembler/Assembler.cs +++ b/Assembler/Assembler.cs @@ -1,21 +1,11 @@ -namespace Q1Emu.Assembler; +namespace Q1.Assembler; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; -using Chip; +using Emulator; +using Emulator.Chip; public class Assembler { - public readonly BinaryWriter Writer; - public readonly StringReader Reader; - - private Dictionary _labels = new(); - private Dictionary _unloadedLabels = new(); - private u16 _addressOffset = 0; - public static Dictionary RegisterLookup = new() { { 0x40, "AX" }, @@ -26,30 +16,31 @@ public class Assembler { 0x45, "DL" }, { 0x46, "LX" }, { 0x50, "PC" }, - { 0x51, "SP" }, + { 0x51, "SP" } }; + private readonly string[] InputLines; + + private readonly Dictionary _labels = new(); - public Assembler(StringReader reader, BinaryWriter writer) + public Assembler(string[] inputLines) { - this.Reader = reader; - this.Writer = writer; + this.InputLines = inputLines; } - public void Assemble() + private void ResolveLabels() { - int lineNumber = 0; - while (this.Reader.ReadLine() is { } line) + int offset = 0; + int pos = 0; + foreach (string line in this.InputLines) { - lineNumber++; - string trimmedLine = line.Split(";")[0].Trim(); // Remove comments + string trimmedLine = line.Split(";")[0].Trim(); if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith(';')) continue; if (trimmedLine.StartsWith(".org")) { - // Handle .org directive string orgValue = trimmedLine[5..].Trim(); - this._addressOffset = this.GetConstant(orgValue); + offset = this.GetConstant(orgValue); } else if (trimmedLine.StartsWith(".ascii")) @@ -61,6 +52,7 @@ public class Assembler { char c = asciiValue[i]; if (c == '\\') + { c = asciiValue[++i] switch { 'n' => '\n', @@ -68,90 +60,173 @@ public class Assembler '\\' => '\\', '\'' => '\'', '"' => '"', - _ => throw new AssemblerException($"Unknown escape sequence: \\{asciiValue[i]}"), + _ => throw new AssemblerException($"Unknown escape sequence: \\{asciiValue[i]}") }; + } - this.Write((u8) c); + pos++; } } else if (trimmedLine.StartsWith(".raw")) { string[] values = trimmedLine[5..].Split(','); - foreach (string value in values) - this.Write(GetConstant(value.Trim())); + pos += values.Length * 2; // Each value is 2 bytes } - + else if (trimmedLine.StartsWith(".include")) { string includeFile = trimmedLine[9..].Trim()[1..^1]; if (!File.Exists(includeFile)) - { - WriteError(line, lineNumber, $"Include file '{includeFile}' not found"); - return; - } + throw new AssemblerException($"Include file '{includeFile}' not found"); - foreach (byte b in File.ReadAllBytes(includeFile)) - this.Writer.Write(b); + pos += File.ReadAllBytes(includeFile).Length; } else if (trimmedLine.EndsWith(":")) - this._labels[trimmedLine[..^1]] = (u16) (this.Writer.BaseStream.Position + this._addressOffset); + { + string label = trimmedLine[..^1].Trim(); + if (this._labels.ContainsKey(label)) + throw new AssemblerException($"Label '{label}' already defined"); + this._labels[label] = (u16) (pos + offset); + } else + pos += this.GetInstructionLength(trimmedLine); + } + } + + public IEnumerable Assemble() + { + this.ResolveLabels(); + + foreach (string line in this.InputLines) + { + string trimmedLine = line.Split(";")[0].Trim(); // Remove comments + if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith(';')) + continue; + + if (trimmedLine.StartsWith(".org")) + { + // Already handled in ResolveLabels + } + + else if (trimmedLine.StartsWith(".ascii")) { - try + string asciiValue = trimmedLine[7..].Trim() + [1..^1]; // Remove quotes + + for (int i = 0; i < asciiValue.Length; i++) { - this.ParseInstruction(trimmedLine); + char c = asciiValue[i]; + if (c == '\\') + { + c = asciiValue[++i] switch + { + 'n' => '\n', + 't' => '\t', + '\\' => '\\', + '\'' => '\'', + '"' => '"', + _ => throw new AssemblerException($"Unknown escape sequence: \\{asciiValue[i]}") + }; + } + + yield return (byte) c; } - catch (AssemblerException ex) + } + + else if (trimmedLine.StartsWith(".raw")) + { + string[] values = trimmedLine[5..].Split(','); + foreach (string value in values) { - WriteError(line, lineNumber, ex.Message); - return; + u16 c = this.GetConstant(value.Trim()); + + yield return c.High(); + yield return c.Low(); } } - } - foreach ((u16 key, string value) in this._unloadedLabels) - { - if (this._labels.TryGetValue(value, out u16 address)) + else if (trimmedLine.StartsWith(".include")) { - Console.WriteLine($"Resolving label '{value}' at address {address:X4} for key {key:X4}"); - this.Writer.BaseStream.Seek(key, SeekOrigin.Begin); - this.Write(address); + string includeFile = trimmedLine[9..].Trim()[1..^1]; + if (!File.Exists(includeFile)) + throw new AssemblerException($"Include file '{includeFile}' not found"); + + foreach (byte b in File.ReadAllBytes(includeFile)) + yield return b; } - else + + else if (trimmedLine.EndsWith(":")) { - WriteError($"", 0, "Label not defined"); + // Handled in ResolveLabels } + + else + foreach (byte b in this.GetInstructionBytes(trimmedLine)) + yield return b; } } - private void WriteError(string line, int lineNumber, string message) + private int GetInstructionLength(string instruction) { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"Error on line {lineNumber}: {message}"); - Console.WriteLine($" --> {line}"); - Console.ResetColor(); + string[] parts = instruction.Split(' ', 2); + string opcode = parts[0].ToUpperInvariant(); + + parts = parts.Length > 1 ? parts[1].Split(',') : []; + if (parts.Length > 2) + throw new AssemblerException("Invalid instruction format"); + + u8 opcodeValue = this.ResolveOpcode(opcode, parts.Length, out u8 addressingIndex); + (u8 m1, u8 m2) = this.ResolveAddressingModes(addressingIndex, parts); + + int length = 2; // Opcode + mode byte + + if (parts.Length > 0) + length += this.GetModeLength(m1); + + if (parts.Length > 1) + length += this.GetModeLength(m2); + + return length; } - private void ParseInstruction(string instruction) + private IEnumerable GetInstructionBytes(string instruction) { string[] parts = instruction.Split(' ', 2); string opcode = parts[0].ToUpperInvariant(); parts = parts.Length > 1 ? parts[1].Split(',') : []; if (parts.Length > 2) - throw new AssemblerException($"Invalid instruction format"); + throw new AssemblerException("Invalid instruction format"); + + u8 opcodeValue = this.ResolveOpcode(opcode, parts.Length, out u8 addressingIndex); + (u8 m1, u8 m2) = this.ResolveAddressingModes(addressingIndex, parts); + + yield return opcodeValue; + yield return (u8) ((m1 << 4) | m2); // Combine m1 and m2 into a single byte - u8 opcodeValue = this.ParseOpcode(opcode, parts.Length, out u8 addressingIndex); + if (parts.Length > 0) + { + foreach (byte b in this.GetModeBytes(m1, parts[0])) + yield return b; + } + if (parts.Length > 1) + { + foreach (byte b in this.GetModeBytes(m2, parts[1])) + yield return b; + } + } + private (u8 m1, u8 m2) ResolveAddressingModes(u8 addressingIndex, string[] parts) + { u8 m1, m2; switch (addressingIndex) { case 0: // Implicit if (parts.Length > 0) - throw new AssemblerException($"Implicit instruction should not have operands"); + throw new AssemblerException("Implicit instruction should not have operands"); m1 = 0x00; m2 = 0x00; @@ -159,17 +234,17 @@ public class Assembler case 1: // Simple addressing mode if (parts.Length > 2) - throw new AssemblerException($"Simple addressing mode should have at most one operand"); + throw new AssemblerException("Simple addressing mode should have at most one operand"); if (parts.Length == 0) - throw new AssemblerException($"Simple addressing mode requires an operand"); + throw new AssemblerException("Simple addressing mode requires an operand"); - m1 = this.ParseMode(parts[0]); + m1 = this.ResolveMode(parts[0]); m2 = 0x00; break; case 2: // Extended implicit if (parts.Length > 0) - throw new AssemblerException($"Extended implicit instruction should not have operands"); + throw new AssemblerException("Extended implicit instruction should not have operands"); m1 = 0x00; m2 = 0x01; @@ -177,25 +252,19 @@ public class Assembler case 3: // Dual addressing mode if (parts.Length != 2) - throw new AssemblerException($"Dual addressing mode requires exactly two operands"); + throw new AssemblerException("Dual addressing mode requires exactly two operands"); - m1 = this.ParseMode(parts[0]); - m2 = this.ParseMode(parts[1]); + m1 = this.ResolveMode(parts[0]); + m2 = this.ResolveMode(parts[1]); break; default: throw new UnreachableException("Invalid addressing index: " + addressingIndex); } - - this.WriteInstruction(opcodeValue, m1, m2); - - if (parts.Length > 0) - this.WriteMode(m1, parts[0]); - if (parts.Length > 1) - this.WriteMode(m2, parts[1]); + return (m1, m2); } - private u8 ParseOpcode(string opcode, int parts, out u8 addressingIndex) + private u8 ResolveOpcode(string opcode, int parts, out u8 addressingIndex) { List addressingIndices = new(); u8? opCode = null; @@ -231,7 +300,7 @@ public class Assembler return opCode.Value; } - private u8 ParseMode(string mode) + private u8 ResolveMode(string mode) { mode = mode.Trim(); @@ -244,14 +313,10 @@ public class Assembler } if (this._labels.ContainsKey(mode)) - { return 0x01; // Immediate - } if (mode.StartsWith("@")) - { return 0x03; // Indirect - } if (mode.StartsWith("[") && mode.EndsWith("]")) { @@ -271,13 +336,7 @@ public class Assembler return 0x01; // Immediate } - private void WriteInstruction(u8 opcode, u8 m1, u8 m2) - { - Write(opcode); - Write((u8) (m1 << 4 | m2)); // Combine m1 and m2 into a single byte - } - - private void WriteMode(u8 mode, string operand) + private byte[] GetModeBytes(u8 mode, string operand) { operand = operand.Trim(); @@ -285,53 +344,66 @@ public class Assembler { case 0x00: // Implicit // No additional data needed - break; + return []; case 0x01: // Immediate u16 immediateValue = this.GetConstant(operand); - this.Write(immediateValue); - break; + return [immediateValue.High(), immediateValue.Low()]; case 0x02: // Direct u16 directAddress = this.GetConstant(operand); // Remove brackets - this.Write(directAddress); - break; + return [directAddress.High(), directAddress.Low()]; case 0x03: // Indirect u16 indirectAddress = this.GetConstant(operand); // Remove brackets and @ - this.Write(indirectAddress); - break; + return [indirectAddress.High(), indirectAddress.Low()]; case 0x04: // Relative - i16 relativeOffset = this.GetSignedConstant(operand); - Write(relativeOffset); - break; + u16 relativeOffset = this.GetConstant(operand); + return [relativeOffset.High(), relativeOffset.Low()]; case 0x05: // Register u8 regIndex = this.GetRegisterIndex(operand); - Write(regIndex); - break; + return [regIndex]; case 0x06: // Register Indirect u8 indirectRegIndex = this.GetRegisterIndex(operand); // Remove brackets - Write(indirectRegIndex); - break; + return [indirectRegIndex]; case 0x07: // Register Indirect with Offset u8 offsetRegIndex = this.GetRegisterIndex(operand[1..^1].Split('+')[0].Trim()); - i16 offsetValue = this.GetSignedConstant(operand[1..^1].Split('+')[1].Trim()); - Write(offsetRegIndex); - Write(offsetValue); - break; + u16 offsetValue = this.GetConstant(operand[1..^1].Split('+')[1].Trim()); + return [offsetRegIndex, offsetValue.High(), offsetValue.Low()]; + + default: + throw new AssemblerException($"Unknown addressing mode: {mode:X2}"); } } + + private int GetModeLength(u8 mode) + { + return mode switch + { + 0x00 => 0, // Implicit + 0x01 => 2, // Immediate + 0x02 => 2, // Direct + 0x03 => 2, // Indirect + 0x04 => 2, // Relative + 0x05 => 1, // Register + 0x06 => 1, // Register indirect + 0x07 => 3, // Register indirect with offset + _ => throw new AssemblerException($"Unknown addressing mode: {mode:X2}") + }; + } private u8 GetRegisterIndex(string reg) { reg = this.Clean(reg); if (reg.StartsWith("V")) + { if (u8.TryParse(reg[1..], out u8 index) && index < 0x3F) return index; + } if (Assembler.RegisterLookup.ContainsValue(reg)) { @@ -351,32 +423,15 @@ public class Assembler u16 x = Convert.ToUInt16(value[1..], 16); return x; } - else if (u16.TryParse(value, out u16 result)) + if (u16.TryParse(value, out u16 result)) // Decimal constant return result; - else - { - this._unloadedLabels[(u16) this.Writer.BaseStream.Position] = value; - return 0; - } - } - private i16 GetSignedConstant(string value) - { - value = this.Clean(value); - - if (value.StartsWith("$")) - { - // Hexadecimal offset - return Convert.ToInt16(value[1..], 16); - } - else if (i16.TryParse(value, out i16 result)) - // Decimal offset - return result; - else - { - this._unloadedLabels[(u16) this.Writer.BaseStream.Position] = value; - return 0; - } + + if (this._labels.TryGetValue(value, out u16 labelAddress)) + // Label reference + return labelAddress; + + return 0; } private string Clean(string value) @@ -389,23 +444,6 @@ public class Assembler return value.Trim(); } - - private void Write(u16 value) - { - u16 swappedValue = (u16) ((value >> 8) | (value << 8)); - this.Writer.Write(swappedValue); - } - - private void Write(u8 value) - { - this.Writer.Write(value); - } - - private void Write(i16 value) - { - u16 swappedValue = (u16) ((value >> 8) | (value << 8)); - this.Writer.Write(swappedValue); - } } public class AssemblerException(string message) diff --git a/Assembler/Assembler.csproj b/Assembler/Assembler.csproj new file mode 100644 index 0000000..fd4f4fa --- /dev/null +++ b/Assembler/Assembler.csproj @@ -0,0 +1,19 @@ + + + + Exe + net9.0 + enable + enable + Q1.Assembler + + + + + + + + + + + diff --git a/Assembler/Disassembler.cs b/Assembler/Disassembler.cs deleted file mode 100644 index 816ddbc..0000000 --- a/Assembler/Disassembler.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace Q1Emu.Assembler; - -using System; -using System.Collections.Generic; -using System.IO; -using Chip; - -public class Disassembler -{ - public readonly StringWriter Writer; - public readonly BinaryReader Reader; - - public Disassembler(StringWriter writer, BinaryReader reader) - { - this.Writer = writer; - this.Reader = reader; - } - - private static Dictionary ModeLookup = new() - { - { 0x00, "" }, - { 0x01, "c" }, - { 0x02, "[a]" }, - { 0x03, "@[a]" }, - { 0x04, "pc + c" }, - { 0x05, "r" }, - { 0x06, "[r]" }, - { 0x07, "[r + c]" }, - }; - - public static string ParseInstruction(u16 instruction) - { - u8 op = (u8) ((instruction >> 8) & 0xFF); // 256 opcodes - u8 m1 = (u8) ((instruction >> 4) & 0x0F); // 16 modes - u8 m2 = (u8) (instruction & 0x0F); // 16 modes - - u8 i = (m1, m2) switch - { - (0x00, 0x00) => 0, // Implicit - (0x00, 0x01) => 2, // Extended implicit - (_, 0x00) => 1, // Simple addressing mode - _ => 3 // Extended addressing mode - }; - - string opcodeName = GetOpcodeName(op, i); - - if (i == 0 || i == 2) - return opcodeName; - - if (i == 1) - return $"{opcodeName} {GetModeName(m1)}"; - - if (i == 3) - return $"{opcodeName} {GetModeName(m1)}, {GetModeName(m2)}"; - - throw new InvalidOperationException($"Unknown instruction format for opcode {op:X2} with modes {m1:X2}, {m2:X2}"); - } - - private static string GetOpcodeName(u8 opcode, u8 i) - { - if (!Q1Cpu.InstructionLookupStrings.TryGetValue(opcode, out string[]? opNames)) - return $"[{opcode:X2}]"; // Fallback for unknown opcodes - - return opNames[i]; - } - - private static string GetModeName(u8 mode) - { - if (ModeLookup.TryGetValue(mode, out string? modeName)) - return modeName; - - return $"[{mode:X2}]"; // Fallback for unknown modes - } -} \ No newline at end of file diff --git a/Assembler/Extensions.cs b/Assembler/Extensions.cs new file mode 100644 index 0000000..fd4ea1f --- /dev/null +++ b/Assembler/Extensions.cs @@ -0,0 +1,15 @@ +namespace Q1.Assembler; + +public static class Extensions +{ + + public static u8 High(this u16 value) + { + return (u8)(value >> 8); + } + + public static u8 Low(this u16 value) + { + return (u8)(value & 0xFF); + } +} \ No newline at end of file diff --git a/Assembler/GlobalUsings.cs b/Assembler/GlobalUsings.cs new file mode 100644 index 0000000..ff38a75 --- /dev/null +++ b/Assembler/GlobalUsings.cs @@ -0,0 +1,12 @@ +// Integer type aliases +global using i8 = sbyte; // -128 to 127 +global using u8 = byte; // 0 to 255 +global using i16 = short; // -32,768 to 32,767 +global using u16 = ushort; // 0 to 65,535 +global using i32 = int; // -2,147,483,648 to 2,147,483,647 +global using u32 = uint; // 0 to 4,294,967,295 +global using i64 = long; // -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 +global using u64 = ulong; // 0 to 18,446,744,073,709,551,615 + +global using ref8 = System.Span; +global using data8 = byte[]; diff --git a/Assembler/Options.cs b/Assembler/Options.cs new file mode 100644 index 0000000..dc4efc2 --- /dev/null +++ b/Assembler/Options.cs @@ -0,0 +1,12 @@ +namespace Q1.Assembler; + +using CommandLine; + +public class Options +{ + [Option('i', "input", Required = true, HelpText = "Input file to assemble.")] + public string InputFile { get; set; } + + [Option('o', "output", Required = true, HelpText = "Output file to write the assembled code.")] + public string OutputFile { get; set; } +} \ No newline at end of file diff --git a/Assembler/Program.cs b/Assembler/Program.cs new file mode 100644 index 0000000..b30df1e --- /dev/null +++ b/Assembler/Program.cs @@ -0,0 +1,38 @@ +namespace Q1.Assembler; + +using CommandLine; + +public class Program +{ + static void Main(string[] args) + { + Parser.Default.ParseArguments(args).WithParsed(Program.Main); + } + + static void Main(Options options) + { + Environment.CurrentDirectory = Path.GetDirectoryName(options.InputFile) ?? Environment.CurrentDirectory; + options.InputFile = Path.GetFileName(options.InputFile); + + string[] input = File.ReadAllLines(options.InputFile); + + try + { + Assembler assembler = new(input); + byte[] bin = assembler.Assemble().ToArray(); + if (bin.Length > 8 * 1024) + throw new AssemblerException("Assembled binary exceeds maximum size of 8KB."); + + File.WriteAllBytes(options.OutputFile, bin); + + Console.WriteLine($"Assembled {options.InputFile} to {options.OutputFile} ({bin.Length} bytes)"); + } + catch (AssemblerException ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine($"Error: {ex.Message}"); + Console.ResetColor(); + Environment.Exit(1); + } + } +} \ No newline at end of file -- cgit v1.3.1