feat(core/asm): add instruction encoder

This commit is contained in:
dualfroz committed 2026-08-24 19:11:37 +02:00
1 parent 2004647201
commit 416348a9aa
1 file changed
+362
@@ -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<Token, int> resolver)
{
if (isLongLoad(instruction, resolver))
{
return 4;
}
return 2;
}
public static void emit(InstructionStatement instruction, Func<Token, int> resolver, List<byte> output)
{
string mnemonic = instruction.mnemonic.ToUpperInvariant();
IReadOnlyList<Operand> 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<Token, int> resolver, List<byte> output)
{
IReadOnlyList<Operand> 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<Token, int> resolver, List<byte> output, int immediateBase, int registerBase)
{
requireOperands(instruction, 2);
IReadOnlyList<Operand> 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<byte> output, int op)
{
requireOperands(instruction, 2);
IReadOnlyList<Operand> 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<byte> output, int op)
{
IReadOnlyList<Operand> 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<Token, int> resolver, List<byte> output)
{
requireOperands(instruction, 2);
IReadOnlyList<Operand> 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<Token, int> resolver, List<byte> 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<Token, int> resolver, List<byte> 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<Token, int> resolver, List<byte> 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<byte> 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<Token, int> resolver)
{
if (!string.Equals(instruction.mnemonic, "LD", StringComparison.OrdinalIgnoreCase))
{
return false;
}
IReadOnlyList<Operand> 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<Token, int> resolver)
{
requireOperands(instruction, 1);
return immediate(instruction.operands[0], resolver);
}
static int immediate(Operand operand, Func<Token, int> 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<byte> output, ushort value)
{
output.Add((byte)(value >> 8));
output.Add((byte)(value & 0xFF));
}
}