From 416348a9aab0a0fc599c6dd7c3107f21352f0cae Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 24 Aug 2026 19:11:37 +0200 Subject: [PATCH] feat(core/asm): add instruction encoder --- src/Chip8.Core/Assembly/InstructionEncoder.cs | 362 ++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 src/Chip8.Core/Assembly/InstructionEncoder.cs diff --git a/src/Chip8.Core/Assembly/InstructionEncoder.cs b/src/Chip8.Core/Assembly/InstructionEncoder.cs new file mode 100644 index 0000000..7aa42c6 --- /dev/null +++ b/src/Chip8.Core/Assembly/InstructionEncoder.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; + +namespace Chip8.Core.Assembly; + +public static class InstructionEncoder +{ + public static int measure(InstructionStatement instruction, Func resolver) + { + if (isLongLoad(instruction, resolver)) + { + return 4; + } + return 2; + } + + public static void emit(InstructionStatement instruction, Func resolver, List output) + { + string mnemonic = instruction.mnemonic.ToUpperInvariant(); + IReadOnlyList operands = instruction.operands; + + switch (mnemonic) + { + case "CLS": zeroOperands(instruction); writeWord(output, 0x00E0); return; + case "RET": zeroOperands(instruction); writeWord(output, 0x00EE); return; + case "SCR": zeroOperands(instruction); writeWord(output, 0x00FB); return; + case "SCL": zeroOperands(instruction); writeWord(output, 0x00FC); return; + case "EXIT": zeroOperands(instruction); writeWord(output, 0x00FD); return; + case "LORES": + case "LOW": zeroOperands(instruction); writeWord(output, 0x00FE); return; + case "HIRES": + case "HIGH": zeroOperands(instruction); writeWord(output, 0x00FF); return; + case "AUDIO": zeroOperands(instruction); writeWord(output, 0xF002); return; + + case "SCD": writeWord(output, (ushort)(0x00C0 | nibble(singleImmediate(instruction, resolver), instruction, "SCD"))); return; + case "SCU": writeWord(output, (ushort)(0x00D0 | nibble(singleImmediate(instruction, resolver), instruction, "SCU"))); return; + case "PLANE": writeWord(output, (ushort)(0xF001 | (nibble(singleImmediate(instruction, resolver), instruction, "PLANE") << 8))); return; + + case "JP": encodeJump(instruction, resolver, output); return; + case "CALL": writeWord(output, (ushort)(0x2000 | address(singleImmediate(instruction, resolver), instruction, "CALL"))); return; + + case "SE": encodeSkip(instruction, resolver, output, 0x3000, 0x5000); return; + case "SNE": encodeSkip(instruction, resolver, output, 0x4000, 0x9000); return; + + case "OR": encodeArithmetic(instruction, output, 0x1); return; + case "AND": encodeArithmetic(instruction, output, 0x2); return; + case "XOR": encodeArithmetic(instruction, output, 0x3); return; + case "SUB": encodeArithmetic(instruction, output, 0x5); return; + case "SUBN": encodeArithmetic(instruction, output, 0x7); return; + case "SHR": encodeShift(instruction, output, 0x6); return; + case "SHL": encodeShift(instruction, output, 0xE); return; + + case "ADD": encodeAdd(instruction, resolver, output); return; + case "LD": encodeLoad(instruction, resolver, output); return; + + case "RND": + requireOperands(instruction, 2); + writeWord(output, (ushort)(0xC000 | (register(operands[0], "RND") << 8) | byte8(immediate(operands[1], resolver), instruction, "RND"))); + return; + + case "DRW": + requireOperands(instruction, 3); + writeWord(output, (ushort)(0xD000 | (register(operands[0], "DRW") << 8) | (register(operands[1], "DRW") << 4) | nibble(immediate(operands[2], resolver), instruction, "DRW"))); + return; + + case "SKP": + requireOperands(instruction, 1); + writeWord(output, (ushort)(0xE09E | (register(operands[0], "SKP") << 8))); + return; + + case "SKNP": + requireOperands(instruction, 1); + writeWord(output, (ushort)(0xE0A1 | (register(operands[0], "SKNP") << 8))); + return; + + case "PITCH": + requireOperands(instruction, 1); + writeWord(output, (ushort)(0xF03A | (register(operands[0], "PITCH") << 8))); + return; + + case "SAVE": encodeRange(instruction, output, 0x2); return; + case "LOAD": encodeRange(instruction, output, 0x3); return; + + default: + throw new AsmException("unknown mnemonic '" + instruction.mnemonic + "'", instruction.position); + } + } + + static void encodeJump(InstructionStatement instruction, Func resolver, List output) + { + IReadOnlyList operands = instruction.operands; + if (operands.Count == 1) + { + writeWord(output, (ushort)(0x1000 | address(immediate(operands[0], resolver), instruction, "JP"))); + return; + } + + if (operands.Count == 2 && operands[0].isRegister(0)) + { + writeWord(output, (ushort)(0xB000 | address(immediate(operands[1], resolver), instruction, "JP"))); + return; + } + + throw new AsmException("JP expects an address or 'V0, address'", instruction.position); + } + + static void encodeSkip(InstructionStatement instruction, Func resolver, List output, int immediateBase, int registerBase) + { + requireOperands(instruction, 2); + IReadOnlyList operands = instruction.operands; + int x = register(operands[0], instruction.mnemonic); + + if (operands[1].kind == OperandKind.Register) + { + writeWord(output, (ushort)(registerBase | (x << 8) | (operands[1].registerIndex << 4))); + return; + } + + writeWord(output, (ushort)(immediateBase | (x << 8) | byte8(immediate(operands[1], resolver), instruction, instruction.mnemonic))); + } + + static void encodeArithmetic(InstructionStatement instruction, List output, int op) + { + requireOperands(instruction, 2); + IReadOnlyList operands = instruction.operands; + int x = register(operands[0], instruction.mnemonic); + int y = register(operands[1], instruction.mnemonic); + writeWord(output, (ushort)(0x8000 | (x << 8) | (y << 4) | op)); + } + + static void encodeShift(InstructionStatement instruction, List output, int op) + { + IReadOnlyList operands = instruction.operands; + if (operands.Count is < 1 or > 2) + { + throw new AsmException(instruction.mnemonic + " expects 'Vx' or 'Vx, Vy'", instruction.position); + } + int x = register(operands[0], instruction.mnemonic); + int y = operands.Count == 2 ? register(operands[1], instruction.mnemonic) : 0; + writeWord(output, (ushort)(0x8000 | (x << 8) | (y << 4) | op)); + } + + static void encodeAdd(InstructionStatement instruction, Func resolver, List output) + { + requireOperands(instruction, 2); + IReadOnlyList operands = instruction.operands; + + if (operands[0].kind == OperandKind.IndexRegister) + { + writeWord(output, (ushort)(0xF01E | (register(operands[1], "ADD") << 8))); + return; + } + + int x = register(operands[0], "ADD"); + if (operands[1].kind == OperandKind.Register) + { + writeWord(output, (ushort)(0x8004 | (x << 8) | (operands[1].registerIndex << 4))); + return; + } + + writeWord(output, (ushort)(0x7000 | (x << 8) | byte8(immediate(operands[1], resolver), instruction, "ADD"))); + } + + static void encodeLoad(InstructionStatement instruction, Func resolver, List output) + { + requireOperands(instruction, 2); + Operand destination = instruction.operands[0]; + Operand source = instruction.operands[1]; + + switch (destination.kind) + { + case OperandKind.Register: + encodeLoadIntoRegister(instruction, destination.registerIndex, source, resolver, output); + return; + + case OperandKind.IndexRegister: + encodeLoadIndex(instruction, source, resolver, output); + return; + + case OperandKind.DelayTimer: + writeWord(output, (ushort)(0xF015 | (register(source, "LD DT") << 8))); + return; + + case OperandKind.SoundTimer: + writeWord(output, (ushort)(0xF018 | (register(source, "LD ST") << 8))); + return; + + case OperandKind.FontReg: + writeWord(output, (ushort)(0xF029 | (register(source, "LD F") << 8))); + return; + + case OperandKind.BigFontReg: + writeWord(output, (ushort)(0xF030 | (register(source, "LD HF") << 8))); + return; + + case OperandKind.BcdReg: + writeWord(output, (ushort)(0xF033 | (register(source, "LD B") << 8))); + return; + + case OperandKind.IndexIndirect: + writeWord(output, (ushort)(0xF055 | (register(source, "LD [I]") << 8))); + return; + + case OperandKind.RplReg: + writeWord(output, (ushort)(0xF075 | (register(source, "LD R") << 8))); + return; + + default: + throw new AsmException("unsupported LD destination", instruction.position); + } + } + + static void encodeLoadIntoRegister(InstructionStatement instruction, int x, Operand source, Func resolver, List output) + { + switch (source.kind) + { + case OperandKind.Register: + writeWord(output, (ushort)(0x8000 | (x << 8) | (source.registerIndex << 4))); + return; + case OperandKind.DelayTimer: + writeWord(output, (ushort)(0xF007 | (x << 8))); + return; + case OperandKind.KeyReg: + writeWord(output, (ushort)(0xF00A | (x << 8))); + return; + case OperandKind.IndexIndirect: + writeWord(output, (ushort)(0xF065 | (x << 8))); + return; + case OperandKind.RplReg: + writeWord(output, (ushort)(0xF085 | (x << 8))); + return; + case OperandKind.Immediate: + writeWord(output, (ushort)(0x6000 | (x << 8) | byte8(immediate(source, resolver), instruction, "LD"))); + return; + default: + throw new AsmException("unsupported LD source", instruction.position); + } + } + + static void encodeLoadIndex(InstructionStatement instruction, Operand source, Func resolver, List output) + { + if (source.kind != OperandKind.Immediate) + { + throw new AsmException("LD I expects an address", instruction.position); + } + + int value = immediate(source, resolver); + if (value is < 0 or > 0xFFFF) + { + throw new AsmException("index value 0x" + value.ToString("X") + " out of 16-bit range", instruction.position); + } + + if (value > 0xFFF) + { + writeWord(output, 0xF000); + writeWord(output, (ushort)value); + return; + } + + writeWord(output, (ushort)(0xA000 | value)); + } + + static void encodeRange(InstructionStatement instruction, List output, int op) + { + requireOperands(instruction, 1); + Operand operand = instruction.operands[0]; + if (operand.kind != OperandKind.RegisterRange) + { + throw new AsmException(instruction.mnemonic + " expects a 'Vx-Vy' register range", instruction.position); + } + writeWord(output, (ushort)(0x5000 | (operand.registerIndex << 8) | (operand.rangeEnd << 4) | op)); + } + + static bool isLongLoad(InstructionStatement instruction, Func resolver) + { + if (!string.Equals(instruction.mnemonic, "LD", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + IReadOnlyList operands = instruction.operands; + if (operands.Count != 2 || operands[0].kind != OperandKind.IndexRegister || operands[1].kind != OperandKind.Immediate) + { + return false; + } + return immediate(operands[1], resolver) > 0xFFF; + } + + static int singleImmediate(InstructionStatement instruction, Func resolver) + { + requireOperands(instruction, 1); + return immediate(instruction.operands[0], resolver); + } + + static int immediate(Operand operand, Func resolver) + { + if (operand.kind != OperandKind.Immediate) + { + throw new AsmException("expected a numeric operand but found " + operand, operand.position); + } + return Expression.evaluate(operand.expressionTokens, resolver, operand.position); + } + + static int register(Operand operand, string mnemonic) + { + if (operand.kind != OperandKind.Register) + { + throw new AsmException(mnemonic + " expects a V register but found " + operand, operand.position); + } + return operand.registerIndex; + } + + static int nibble(int value, InstructionStatement instruction, string mnemonic) + { + if (value is < 0 or > 0xF) + { + throw new AsmException(mnemonic + " nibble 0x" + value.ToString("X") + " out of range (0-15)", instruction.position); + } + return value; + } + + static int byte8(int value, InstructionStatement instruction, string mnemonic) + { + if (value is < 0 or > 0xFF) + { + throw new AsmException(mnemonic + " byte 0x" + value.ToString("X") + " out of range (0-255)", instruction.position); + } + return value; + } + + static int address(int value, InstructionStatement instruction, string mnemonic) + { + if (value is < 0 or > 0xFFF) + { + throw new AsmException(mnemonic + " address 0x" + value.ToString("X") + " out of range (0-0xFFF)", instruction.position); + } + return value; + } + + static void requireOperands(InstructionStatement instruction, int count) + { + if (instruction.operands.Count != count) + { + throw new AsmException( + instruction.mnemonic + " expects " + count + " operand(s), got " + instruction.operands.Count, + instruction.position); + } + } + + static void zeroOperands(InstructionStatement instruction) + { + if (instruction.operands.Count != 0) + { + throw new AsmException(instruction.mnemonic + " takes no operands", instruction.position); + } + } + + static void writeWord(List output, ushort value) + { + output.Add((byte)(value >> 8)); + output.Add((byte)(value & 0xFF)); + } +}