task 3.1-3.2: IAssembler interface + StubAssembler emit primitives

Add IAssembler seam (Assemble(text, origin)) with StubAssembler default
backend. StubAssembler provides EmitU8/EmitU32/EmitU64 little-endian
byte emitters (zero dep, no FASM). Assemble throws NotSupportedException
on StubAssembler (text assembly deferred to IcedAssembler, Phase 8).

7 new tests: EmitU8, EmitU32 x2, EmitU64 x2, IS-A check,
Assemble throws. All passing (total: 64).
This commit is contained in:
kbe
2026-07-21 19:35:11 +02:00
parent 68255f907c
commit 855595837f
3 changed files with 144 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
namespace WhiteMagic.Assembly;
/// <summary>
/// Abstraction over an x86/x64 assembler. The default <see cref="StubAssembler"/>
/// hand-emits calling-convention trampolines (no parsing, zero dep). An optional
/// <see cref="IcedAssembler"/> (Phase 8) handles arbitrary mnemonics via the Iced
/// library.
/// </summary>
public interface IAssembler
{
/// <summary>
/// Assembles text mnemonics into machine code.
/// </summary>
/// <param name="assemblyText">The assembly text (Intel syntax).</param>
/// <param name="origin">The base address for relative encodings.</param>
byte[] Assemble(string assemblyText, ulong origin = 0);
}
+49
View File
@@ -0,0 +1,49 @@
namespace WhiteMagic.Assembly;
/// <summary>
/// The default <see cref="IAssembler"/> backend. Hand-emits calling-convention
/// trampolines and injection stubs using deterministic byte emitters
/// (<see cref="EmitU8"/>, <see cref="EmitU32"/>, <see cref="EmitU64"/>). Has
/// no native or third-party dependency — no FASM, no Iced.
/// </summary>
/// <remarks>
/// <see cref="Assemble"/> is not supported by this backend (it is a parse-free
/// emitter, not a text assembler). Use <see cref="IcedAssembler"/> (Phase 8) for
/// arbitrary mnemonics.
/// </remarks>
public class StubAssembler : IAssembler
{
/// <inheritdoc />
/// <exception cref="NotSupportedException">Always thrown. StubAssembler does
/// not parse text assembly; use IcedAssembler for that.</exception>
public byte[] Assemble(string assemblyText, ulong origin = 0)
{
throw new NotSupportedException(
"StubAssembler does not parse text assembly. " +
"Use IcedAssembler (Phase 8) for arbitrary mnemonics.");
}
// ── Emit primitives ────────────────────────────────────────────────────
/// <summary>Appends a single byte to <paramref name="buffer"/>.</summary>
public void EmitU8(List<byte> buffer, byte value)
{
buffer.Add(value);
}
/// <summary>Appends <paramref name="value"/> as 4 little-endian bytes.</summary>
public void EmitU32(List<byte> buffer, uint value)
{
buffer.Add((byte)value);
buffer.Add((byte)(value >> 8));
buffer.Add((byte)(value >> 16));
buffer.Add((byte)(value >> 24));
}
/// <summary>Appends <paramref name="value"/> as 8 little-endian bytes.</summary>
public void EmitU64(List<byte> buffer, ulong value)
{
EmitU32(buffer, (uint)value);
EmitU32(buffer, (uint)(value >> 32));
}
}
+78
View File
@@ -0,0 +1,78 @@
using WhiteMagic.Assembly;
namespace WhiteMagicTest;
/// <summary>
/// Tests for <see cref="StubAssembler"/> emit primitives (<see cref="StubAssembler.EmitU8"/>,
/// <see cref="StubAssembler.EmitU32"/>, <see cref="StubAssembler.EmitU64"/>) and the
/// <see cref="IAssembler"/> interface contract.
/// </summary>
public class StubAssemblerTests
{
private static StubAssembler Create() => new();
// ── Emit primitives ────────────────────────────────────────────────────
[Fact]
public void EmitU8_appends_a_single_byte()
{
var sut = Create();
var buffer = new List<byte>();
sut.EmitU8(buffer, 0xAB);
Assert.Equal([0xAB], buffer);
}
[Fact]
public void EmitU32_appends_little_endian()
{
var sut = Create();
var buffer = new List<byte>();
sut.EmitU32(buffer, 0x11223344);
Assert.Equal([0x44, 0x33, 0x22, 0x11], buffer);
}
[Fact]
public void EmitU32_appends_zero()
{
var sut = Create();
var buffer = new List<byte>();
sut.EmitU32(buffer, 0);
Assert.Equal([0x00, 0x00, 0x00, 0x00], buffer);
}
[Fact]
public void EmitU64_appends_little_endian()
{
var sut = Create();
var buffer = new List<byte>();
sut.EmitU64(buffer, 0x1122334455667788);
byte[] expected = [0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11];
Assert.Equal(expected, buffer);
}
[Fact]
public void EmitU64_appends_high_bits()
{
var sut = Create();
var buffer = new List<byte>();
sut.EmitU64(buffer, 0xDEADBEEF_CAFEBABE);
byte[] expected = [0xBE, 0xBA, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE];
Assert.Equal(expected, buffer);
}
// ── IAssembler interface ───────────────────────────────────────────────
[Fact]
public void StubAssembler_is_an_IAssembler()
{
var sut = Create();
Assert.IsAssignableFrom<IAssembler>(sut);
}
[Fact]
public void Assemble_from_StubAssembler_throws_NotSupported()
{
var sut = Create();
Assert.Throws<NotSupportedException>(() => sut.Assemble("nop", 0));
}
}