diff --git a/src/Chip8.Core/Assembly/Assembler.cs b/src/Chip8.Core/Assembly/Assembler.cs new file mode 100644 index 0000000..1970745 --- /dev/null +++ b/src/Chip8.Core/Assembly/Assembler.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace Chip8.Core.Assembly; + +public sealed class Assembler +{ + const int layoutIterationLimit = 32; + + readonly ushort baseAddress; + + public Assembler(ushort baseAddress = Machine.programStart) + { + this.baseAddress = baseAddress; + } + + public static byte[] assembleRom(string source, ushort baseAddress = Machine.programStart) => + new Assembler(baseAddress).assemble(source).rom; + + public AssemblyResult assemble(string source) + { + List tokens = new Lexer(source).tokenize(); + List expanded = new MacroExpander().expand(tokens); + List statements = new Parser(expanded).parse(); + + Dictionary symbols = resolveSymbols(statements); + return emit(statements, symbols); + } + + Dictionary resolveSymbols(List statements) + { + var symbols = new Dictionary(StringComparer.Ordinal); + + for (int iteration = 0; iteration < layoutIterationLimit; iteration++) + { + var next = new Dictionary(StringComparer.Ordinal); + Func resolver = token => symbols.TryGetValue(token.text, out int value) ? value : 0; + int address = baseAddress; + + foreach (Statement statement in statements) + { + address = layoutStatement(statement, next, resolver, address); + } + + if (sameSymbols(symbols, next)) + { + return next; + } + + symbols = next; + } + + throw new AsmException("symbol layout did not converge (possible circular reference)", new SourcePosition(0, 0)); + } + + int layoutStatement(Statement statement, Dictionary symbols, Func resolver, int address) + { + switch (statement) + { + case LabelStatement label: + define(symbols, label.name, address, label.position); + return address; + + case ConstantStatement constant: + define(symbols, constant.name, Expression.evaluate(constant.expression, resolver, constant.position), constant.position); + return address; + + case OrgStatement org: + return requireForward(Expression.evaluate(org.expression, resolver, org.position), org.position, address); + + case AlignStatement align: + return alignTo(address, Expression.evaluate(align.expression, resolver, align.position), align.position); + + case DataStatement data: + return address + data.items.Count * (data.word ? 2 : 1); + + case InstructionStatement instruction: + return address + InstructionEncoder.measure(instruction, resolver); + + default: + return address; + } + } + + AssemblyResult emit(List statements, Dictionary symbols) + { + Func resolver = token => symbols.TryGetValue(token.text, out int value) + ? value + : throw new AsmException("undefined symbol '" + token.text + "'", token.position); + + var writes = new Dictionary(); + var listing = new StringBuilder(); + int address = baseAddress; + int highest = baseAddress; + + foreach (Statement statement in statements) + { + switch (statement) + { + case LabelStatement label: + listing.Append(labelLine(label.name, address)); + break; + + case ConstantStatement constant: + listing.Append(constantLine(constant.name, symbols[constant.name])); + break; + + case OrgStatement org: + address = requireForward(Expression.evaluate(org.expression, resolver, org.position), org.position, address); + break; + + case AlignStatement align: + address = alignTo(address, Expression.evaluate(align.expression, resolver, align.position), align.position); + break; + + case DataStatement data: + address = emitData(data, resolver, writes, listing, address); + break; + + case InstructionStatement instruction: + address = emitInstruction(instruction, resolver, writes, listing, address); + break; + } + + highest = Math.Max(highest, address); + } + + return new AssemblyResult(buildRom(writes, highest), baseAddress, symbols, listing.ToString()); + } + + int emitData(DataStatement data, Func resolver, Dictionary writes, StringBuilder listing, int address) + { + int start = address; + var bytes = new List(); + + foreach (IReadOnlyList item in data.items) + { + int value = Expression.evaluate(item, resolver, data.position); + if (data.word) + { + if (value is < 0 or > 0xFFFF) + { + throw new AsmException("DW value 0x" + value.ToString("X") + " out of 16-bit range", data.position); + } + bytes.Add((byte)(value >> 8)); + bytes.Add((byte)(value & 0xFF)); + } + else + { + if (value is < -128 or > 0xFF) + { + throw new AsmException("DB value 0x" + value.ToString("X") + " out of byte range", data.position); + } + bytes.Add((byte)(value & 0xFF)); + } + } + + foreach (byte value in bytes) + { + writes[address++] = value; + } + + listing.Append(bytesLine(start, bytes, data.word ? "DW" : "DB")); + return address; + } + + int emitInstruction(InstructionStatement instruction, Func resolver, Dictionary writes, StringBuilder listing, int address) + { + var bytes = new List(); + InstructionEncoder.emit(instruction, resolver, bytes); + + int start = address; + foreach (byte value in bytes) + { + writes[address++] = value; + } + + ushort opcode = (ushort)((bytes[0] << 8) | bytes[1]); + ushort extra = bytes.Count == 4 ? (ushort)((bytes[2] << 8) | bytes[3]) : (ushort)0; + listing.Append(instructionLine(start, bytes, Disassembler.describe(opcode, extra))); + return address; + } + + byte[] buildRom(Dictionary writes, int highest) + { + int length = Math.Max(0, highest - baseAddress); + var rom = new byte[length]; + foreach (KeyValuePair write in writes) + { + rom[write.Key - baseAddress] = write.Value; + } + return rom; + } + + static void define(Dictionary symbols, string name, int value, SourcePosition position) + { + if (symbols.ContainsKey(name)) + { + throw new AsmException("duplicate definition of '" + name + "'", position); + } + symbols[name] = value; + } + + int requireForward(int target, SourcePosition position, int address) + { + if (target < baseAddress) + { + throw new AsmException("ORG 0x" + target.ToString("X") + " is below base 0x" + baseAddress.ToString("X"), position); + } + return target; + } + + static int alignTo(int address, int boundary, SourcePosition position) + { + if (boundary <= 0) + { + throw new AsmException("ALIGN needs a positive boundary", position); + } + int remainder = address % boundary; + return remainder == 0 ? address : address + (boundary - remainder); + } + + static bool sameSymbols(Dictionary left, Dictionary right) + { + if (left.Count != right.Count) + { + return false; + } + foreach (KeyValuePair entry in left) + { + if (!right.TryGetValue(entry.Key, out int value) || value != entry.Value) + { + return false; + } + } + return true; + } + + static string labelLine(string name, int address) => + " " + name + ": (= 0x" + address.ToString("X4", CultureInfo.InvariantCulture) + ")\n"; + + static string constantLine(string name, int value) => + " " + name + " = 0x" + value.ToString("X", CultureInfo.InvariantCulture) + "\n"; + + static string instructionLine(int address, List bytes, string text) => + addressColumn(address) + hexColumn(bytes).PadRight(12) + text + "\n"; + + static string bytesLine(int address, List bytes, string keyword) => + addressColumn(address) + hexColumn(bytes).PadRight(12) + keyword + "\n"; + + static string addressColumn(int address) => + "0x" + address.ToString("X4", CultureInfo.InvariantCulture) + ": "; + + static string hexColumn(List bytes) + { + var builder = new StringBuilder(); + foreach (byte value in bytes) + { + builder.Append(value.ToString("X2", CultureInfo.InvariantCulture)).Append(' '); + } + return builder.ToString().TrimEnd(); + } +} diff --git a/src/Chip8.Core/Assembly/AssemblyResult.cs b/src/Chip8.Core/Assembly/AssemblyResult.cs new file mode 100644 index 0000000..4f89e2e --- /dev/null +++ b/src/Chip8.Core/Assembly/AssemblyResult.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Chip8.Core.Assembly; + +public sealed class AssemblyResult +{ + public AssemblyResult(byte[] rom, ushort baseAddress, IReadOnlyDictionary symbols, string listing) + { + this.rom = rom; + this.baseAddress = baseAddress; + this.symbols = symbols; + this.listing = listing; + } + + public byte[] rom { get; } + public ushort baseAddress { get; } + public IReadOnlyDictionary symbols { get; } + public string listing { get; } +}