Commit 2b5de9403d

Jakub Konka <kubkon@jakubkonka.com>
2021-12-21 16:52:50
stage2: create generic lowering fns for MI, RM, and MR encodings
This way I am hopeful they can be reused for every MIR lowering function which follows a given encoding. Currently, support MI, RM and MR encodings without SIB scaling.
1 parent 5156ccd
Changed files (3)
src/arch/x86_64/Emit.zig
@@ -48,33 +48,6 @@ const InnerError = error{
     EmitFail,
 };
 
-const EmitResult = union(enum) {
-    ok: void,
-    err: *ErrorMsg,
-
-    fn ok() EmitResult {
-        return EmitResult{ .ok = .{} };
-    }
-
-    fn err(
-        allocator: Allocator,
-        src_loc: Module.SrcLoc,
-        comptime format: []const u8,
-        args: anytype,
-    ) error{OutOfMemory}!EmitResult {
-        return EmitResult{
-            .err = try ErrorMsg.create(allocator, src_loc, format, args),
-        };
-    }
-
-    fn deinit(res: EmitResult, allocator: Allocator) void {
-        switch (res) {
-            .ok => {},
-            .err => |err_msg| err_msg.destroy(allocator),
-        }
-    }
-};
-
 const Reloc = struct {
     /// Offset of the instruction.
     source: u64,
@@ -184,15 +157,9 @@ pub fn deinit(emit: *Emit) void {
 }
 
 fn fail(emit: *Emit, comptime format: []const u8, args: anytype) InnerError {
-    @setCold(true);
-    const err_msg = try ErrorMsg.create(emit.bin_file.allocator, emit.src_loc, format, args);
-    return emit.failWithErrorMsg(err_msg);
-}
-
-fn failWithErrorMsg(emit: *Emit, err_msg: *ErrorMsg) InnerError {
     @setCold(true);
     assert(emit.err_msg == null);
-    emit.err_msg = err_msg;
+    emit.err_msg = try ErrorMsg.create(emit.bin_file.allocator, emit.src_loc, format, args);
     return error.EmitFail;
 }
 
@@ -565,7 +532,7 @@ fn mirRet(emit: *Emit, inst: Mir.Inst.Index) InnerError!void {
     }
 }
 
-const EncType = enum {
+const Encoding = enum {
     /// OP r/m64, imm32
     mi,
 
@@ -578,11 +545,11 @@ const EncType = enum {
 
 const OpCode = struct {
     opc: u8,
-    /// Only used if `EncType == .mi`.
+    /// Only used if `Encoding == .mi`.
     modrm_ext: u3,
 };
 
-inline fn getArithOpCode(tag: Mir.Inst.Tag, enc: EncType) OpCode {
+inline fn getOpCode(tag: Mir.Inst.Tag, enc: Encoding) OpCode {
     switch (enc) {
         .mi => return switch (tag) {
             .adc => .{ .opc = 0x81, .modrm_ext = 0x2 },
@@ -629,247 +596,298 @@ inline fn getArithOpCode(tag: Mir.Inst.Tag, enc: EncType) OpCode {
     }
 }
 
-fn mirArith(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerError!void {
-    const res = try mirArithImpl(
-        emit.bin_file.allocator,
-        tag,
-        emit.mir.instructions,
-        emit.mir.extra,
-        inst,
-        emit.src_loc,
-        emit.code,
-    );
-    switch (res) {
-        .ok => {},
-        .err => |err_msg| return emit.failWithErrorMsg(err_msg),
+const ScaleIndexBase = struct {
+    scale: u2,
+    index_reg: ?Register,
+    base_reg: ?Register,
+};
+
+const Memory = struct {
+    reg: ?Register,
+    disp: i32,
+    sib: ?ScaleIndexBase = null,
+};
+
+const RegisterOrMemory = union(enum) {
+    register: Register,
+    memory: Memory,
+
+    fn reg(register: Register) RegisterOrMemory {
+        return .{ .register = register };
     }
-}
 
-fn mirArithImpl(
-    allocator: Allocator,
+    fn mem(register: ?Register, disp: i32) RegisterOrMemory {
+        return .{
+            .memory = .{
+                .reg = register,
+                .disp = disp,
+            },
+        };
+    }
+};
+
+fn lowerToMiEnc(
     tag: Mir.Inst.Tag,
-    mir_instructions: std.MultiArrayList(Mir.Inst).Slice,
-    mir_extra: []const u32,
-    inst: Mir.Inst.Index,
-    src_loc: Module.SrcLoc,
+    reg_or_mem: RegisterOrMemory,
+    imm: i32,
     code: *std.ArrayList(u8),
-) error{OutOfMemory}!EmitResult {
-    const ops = Mir.Ops.decode(mir_instructions.items(.ops)[inst]);
-    switch (ops.flags) {
-        0b00 => blk: {
-            if (ops.reg2 == .none) {
-                // mov reg1, imm32
-                // MI
-                const imm = mir_instructions.items(.data)[inst].imm;
-                const opcode = getArithOpCode(tag, .mi);
-                const opc: u8 = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
-                const encoder = try Encoder.init(code, 7);
-                if (ops.reg1.size() == 16) {
-                    // 0x66 prefix switches to the non-default size; here we assume a switch from
-                    // the default 32bits to 16bits operand-size.
-                    // More info: https://www.cs.uni-potsdam.de/desn/lehre/ss15/64-ia-32-architectures-software-developer-instruction-set-reference-manual-325383.pdf#page=32&zoom=auto,-159,773
-                    encoder.opcode_1byte(0x66);
-                }
+) InnerError!void {
+    const opcode = getOpCode(tag, .mi);
+    switch (reg_or_mem) {
+        .register => |dst_reg| {
+            const opc: u8 = if (dst_reg.size() == 8) opcode.opc - 1 else opcode.opc;
+            const encoder = try Encoder.init(code, 7);
+            if (dst_reg.size() == 16) {
+                // 0x66 prefix switches to the non-default size; here we assume a switch from
+                // the default 32bits to 16bits operand-size.
+                // More info: https://www.cs.uni-potsdam.de/desn/lehre/ss15/64-ia-32-architectures-software-developer-instruction-set-reference-manual-325383.pdf#page=32&zoom=auto,-159,773
+                encoder.opcode_1byte(0x66);
+            }
+            encoder.rex(.{
+                .w = dst_reg.size() == 64,
+                .b = dst_reg.isExtended(),
+            });
+            encoder.opcode_1byte(opc);
+            encoder.modRm_direct(opcode.modrm_ext, dst_reg.lowId());
+            switch (dst_reg.size()) {
+                8 => {
+                    const imm8 = try math.cast(i8, imm);
+                    encoder.imm8(imm8);
+                },
+                16 => {
+                    const imm16 = try math.cast(i16, imm);
+                    encoder.imm16(imm16);
+                },
+                32, 64 => encoder.imm32(imm),
+                else => unreachable,
+            }
+        },
+        .memory => |dst_mem| {
+            const encoder = try Encoder.init(code, 12);
+            if (dst_mem.reg) |dst_reg| {
+                // Register dst_reg can either be 64bit or 32bit in size.
+                // TODO for memory operand, immediate operand pair, we currently
+                // have no way of flagging whether the immediate can be 8-, 16- or
+                // 32-bit and whether the corresponding memory operand is respectively
+                // a byte, word or dword ptr.
+                // TODO we currently don't have a way to flag imm32 64bit sign extended
+                if (dst_reg.size() != 64) return error.EmitFail;
                 encoder.rex(.{
-                    .w = ops.reg1.size() == 64,
-                    .b = ops.reg1.isExtended(),
+                    .w = false,
+                    .b = dst_reg.isExtended(),
                 });
-                encoder.opcode_1byte(opc);
-                encoder.modRm_direct(opcode.modrm_ext, ops.reg1.lowId());
-                switch (ops.reg1.size()) {
-                    8 => {
-                        const imm8 = math.cast(i8, imm) catch {
-                            return EmitResult.err(
-                                allocator,
-                                src_loc,
-                                "size mismatch: sizeof {} != sizeof 0x{x}",
-                                .{
-                                    ops.reg1,
-                                    imm,
-                                },
-                            );
-                        };
-                        encoder.imm8(imm8);
-                    },
-                    16 => {
-                        const imm16 = math.cast(i16, imm) catch {
-                            return EmitResult.err(
-                                allocator,
-                                src_loc,
-                                "size mismatch: sizeof {} != sizeof 0x{x}",
-                                .{
-                                    ops.reg1,
-                                    imm,
-                                },
-                            );
-                        };
-                        encoder.imm16(imm16);
-                    },
-                    32, 64 => {
-                        encoder.imm32(imm);
-                    },
-                    else => unreachable,
+                encoder.opcode_1byte(opcode.opc);
+                if (dst_mem.disp == 0) {
+                    encoder.modRm_indirectDisp0(opcode.modrm_ext, dst_reg.lowId());
+                } else if (immOpSize(dst_mem.disp) == 8) {
+                    encoder.modRm_indirectDisp8(opcode.modrm_ext, dst_reg.lowId());
+                    encoder.disp8(@intCast(i8, dst_mem.disp));
+                } else {
+                    if (dst_reg.lowId() == 4) {
+                        encoder.modRm_SIBDisp32(opcode.modrm_ext);
+                        encoder.sib_baseDisp32(dst_reg.lowId());
+                        encoder.disp32(dst_mem.disp);
+                    } else {
+                        encoder.modRm_indirectDisp32(opcode.modrm_ext, dst_reg.lowId());
+                        encoder.disp32(dst_mem.disp);
+                    }
                 }
-                break :blk;
-            }
-            // mov reg1, reg2
-            // MR
-            if (ops.reg1.size() != ops.reg2.size()) {
-                return EmitResult.err(allocator, src_loc, "size mismatch: sizeof {} != sizeof {}", .{
-                    ops.reg1,
-                    ops.reg2,
-                });
+            } else {
+                encoder.opcode_1byte(opcode.opc);
+                encoder.modRm_SIBDisp0(opcode.modrm_ext);
+                encoder.sib_disp32();
+                encoder.disp32(dst_mem.disp);
             }
-            const opcode = getArithOpCode(tag, .mr);
-            const opc: u8 = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
+            encoder.imm32(imm);
+        },
+    }
+}
+
+fn lowerToRmEnc(
+    tag: Mir.Inst.Tag,
+    reg: Register,
+    reg_or_mem: RegisterOrMemory,
+    code: *std.ArrayList(u8),
+) InnerError!void {
+    const opcode = getOpCode(tag, .rm);
+    const opc: u8 = if (reg.size() == 8) opcode.opc - 1 else opcode.opc;
+    switch (reg_or_mem) {
+        .register => |src_reg| {
+            if (reg.size() != src_reg.size()) return error.EmitFail;
             const encoder = try Encoder.init(code, 3);
             encoder.rex(.{
-                .w = ops.reg1.size() == 64 and ops.reg2.size() == 64,
-                .r = ops.reg2.isExtended(),
-                .b = ops.reg1.isExtended(),
+                .w = reg.size() == 64,
+                .r = reg.isExtended(),
+                .b = src_reg.isExtended(),
             });
             encoder.opcode_1byte(opc);
-            encoder.modRm_direct(ops.reg2.lowId(), ops.reg1.lowId());
+            encoder.modRm_direct(reg.lowId(), src_reg.lowId());
         },
-        0b01 => blk: {
-            const imm = mir_instructions.items(.data)[inst].imm;
-            const opcode = getArithOpCode(tag, .rm);
-            const opc: u8 = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
-            if (ops.reg2 == .none) {
-                // mov reg1, [imm32]
-                // RM
-                const encoder = try Encoder.init(code, 9);
-                if (ops.reg1.size() == 16) {
-                    encoder.opcode_1byte(0x66);
+        .memory => |src_mem| {
+            const encoder = try Encoder.init(code, 9);
+            if (reg.size() == 16) {
+                encoder.opcode_1byte(0x66);
+            }
+            if (src_mem.reg) |src_reg| {
+                // TODO handle 32-bit base register - requires prefix 0x67
+                // Intel Manual, Vol 1, chapter 3.6 and 3.6.1
+                if (src_reg.size() != 64) return error.EmitFail;
+                encoder.rex(.{
+                    .w = reg.size() == 64,
+                    .r = reg.isExtended(),
+                    .b = src_reg.isExtended(),
+                });
+                encoder.opcode_1byte(opc);
+                if (src_mem.disp == 0) {
+                    encoder.modRm_indirectDisp0(reg.lowId(), src_reg.lowId());
+                } else if (immOpSize(src_mem.disp) == 8) {
+                    encoder.modRm_indirectDisp8(reg.lowId(), src_reg.lowId());
+                    encoder.disp8(@intCast(i8, src_mem.disp));
+                } else {
+                    if (src_reg.lowId() == 4) {
+                        encoder.modRm_SIBDisp32(reg.lowId());
+                        encoder.sib_baseDisp32(src_reg.lowId());
+                        encoder.disp32(src_mem.disp);
+                    } else {
+                        encoder.modRm_indirectDisp32(reg.lowId(), src_reg.lowId());
+                        encoder.disp32(src_mem.disp);
+                    }
                 }
+            } else {
                 encoder.rex(.{
-                    .w = ops.reg1.size() == 64,
-                    .r = ops.reg1.isExtended(),
+                    .w = reg.size() == 64,
+                    .r = reg.isExtended(),
                 });
                 encoder.opcode_1byte(opc);
-                encoder.modRm_SIBDisp0(ops.reg1.lowId());
+                encoder.modRm_SIBDisp0(reg.lowId());
                 encoder.sib_disp32();
-                encoder.disp32(imm);
-                break :blk;
-            }
-            // mov reg1, [reg2 + imm32]
-            // RM
-            // TODO handle 32-bit base register - requires prefix 0x67
-            // Intel Manual, Vol 1, chapter 3.6 and 3.6.1
-            if (ops.reg2.size() != 64) {
-                return EmitResult.err(allocator, src_loc, "size mismatch: sizeof {} != 8", .{ops.reg2});
-            }
-            const encoder = try Encoder.init(code, 8);
-            if (ops.reg1.size() == 16) {
-                encoder.opcode_1byte(0x66);
+                encoder.disp32(src_mem.disp);
             }
+        },
+    }
+}
+
+fn lowerToMrEnc(
+    tag: Mir.Inst.Tag,
+    reg_or_mem: RegisterOrMemory,
+    reg: Register,
+    code: *std.ArrayList(u8),
+) InnerError!void {
+    // We use size of source register reg to work out which
+    // variant of memory ptr to pick:
+    // * reg is 64bit - qword ptr
+    // * reg is 32bit - dword ptr
+    // * reg is 16bit - word ptr
+    // * reg is 8bit - byte ptr
+    const opcode = getOpCode(tag, .mr);
+    const opc: u8 = if (reg.size() == 8) opcode.opc - 1 else opcode.opc;
+    switch (reg_or_mem) {
+        .register => |dst_reg| {
+            if (dst_reg.size() != reg.size()) return error.EmitFail;
+            const encoder = try Encoder.init(code, 3);
             encoder.rex(.{
-                .w = ops.reg1.size() == 64,
-                .r = ops.reg1.isExtended(),
-                .b = ops.reg2.isExtended(),
+                .w = dst_reg.size() == 64,
+                .r = reg.isExtended(),
+                .b = dst_reg.isExtended(),
             });
             encoder.opcode_1byte(opc);
-            if (immOpSize(imm) == 8) {
-                encoder.modRm_indirectDisp8(ops.reg1.lowId(), ops.reg2.lowId());
-                encoder.disp8(@intCast(i8, imm));
+            encoder.modRm_direct(reg.lowId(), dst_reg.lowId());
+        },
+        .memory => |dst_mem| {
+            const encoder = try Encoder.init(code, 9);
+            if (reg.size() == 16) {
+                encoder.opcode_1byte(0x66);
+            }
+            if (dst_mem.reg) |dst_reg| {
+                if (dst_reg.size() != 64) return error.EmitFail;
+                encoder.rex(.{
+                    .w = reg.size() == 64,
+                    .r = reg.isExtended(),
+                    .b = dst_reg.isExtended(),
+                });
+                encoder.opcode_1byte(opc);
+                if (dst_mem.disp == 0) {
+                    encoder.modRm_indirectDisp0(reg.lowId(), dst_reg.lowId());
+                } else if (immOpSize(dst_mem.disp) == 8) {
+                    encoder.modRm_indirectDisp8(reg.lowId(), dst_reg.lowId());
+                    encoder.disp8(@intCast(i8, dst_mem.disp));
+                } else {
+                    if (dst_reg.lowId() == 4) {
+                        encoder.modRm_SIBDisp32(reg.lowId());
+                        encoder.sib_baseDisp32(dst_reg.lowId());
+                        encoder.disp32(dst_mem.disp);
+                    } else {
+                        encoder.modRm_indirectDisp32(reg.lowId(), dst_reg.lowId());
+                        encoder.disp32(dst_mem.disp);
+                    }
+                }
             } else {
-                encoder.modRm_indirectDisp32(ops.reg1.lowId(), ops.reg2.lowId());
-                encoder.disp32(imm);
+                encoder.rex(.{
+                    .w = reg.size() == 64,
+                    .r = reg.isExtended(),
+                });
+                encoder.opcode_1byte(opc);
+                encoder.modRm_SIBDisp0(reg.lowId());
+                encoder.sib_disp32();
+                encoder.disp32(dst_mem.disp);
             }
         },
-        0b10 => blk: {
-            // TODO handle 32-bit base register - requires prefix 0x67
-            // Intel Manual, Vol 1, chapter 3.6 and 3.6.1
-            if (ops.reg1.size() != 64) {
-                return EmitResult.err(allocator, src_loc, "size mismatch: sizeof {} != 8", .{ops.reg1});
+    }
+}
+
+fn mirArith(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerError!void {
+    const ops = Mir.Ops.decode(emit.mir.instructions.items(.ops)[inst]);
+    switch (ops.flags) {
+        0b00 => {
+            if (ops.reg2 == .none) {
+                // mov reg1, imm32
+                // MI
+                const imm = emit.mir.instructions.items(.data)[inst].imm;
+                return lowerToMiEnc(tag, RegisterOrMemory.reg(ops.reg1), imm, emit.code);
+            }
+            // mov reg1, reg2
+            // RM
+            return lowerToRmEnc(tag, ops.reg1, RegisterOrMemory.reg(ops.reg2), emit.code);
+        },
+        0b01 => {
+            const imm = emit.mir.instructions.items(.data)[inst].imm;
+            if (ops.reg2 == .none) {
+                // mov reg1, [imm32]
+                // RM
+                return lowerToRmEnc(tag, ops.reg1, RegisterOrMemory.mem(null, imm), emit.code);
             }
+            // mov reg1, [reg2 + imm32]
+            // RM
+            return lowerToRmEnc(tag, ops.reg1, RegisterOrMemory.mem(ops.reg2, imm), emit.code);
+        },
+        0b10 => {
             if (ops.reg2 == .none) {
-                // mov [reg1 + 0], imm32
+                // mov dword ptr [reg1 + 0], imm32
                 // MI
-                // Base register reg1 can either be 64bit or 32bit in size.
-                // TODO for memory operand, immediate operand pair, we currently
-                // have no way of flagging whether the immediate can be 8-, 16- or
-                // 32-bit and whether the corresponding memory operand is respectively
-                // a byte, word or dword ptr.
-                // TODO we currently don't have a way to flag imm32 64bit sign extended
-                const imm = mir_instructions.items(.data)[inst].imm;
-                const opcode = getArithOpCode(tag, .mi);
-                const encoder = try Encoder.init(code, 7);
-                encoder.rex(.{
-                    .w = false,
-                    .b = ops.reg1.isExtended(),
-                });
-                encoder.opcode_1byte(opcode.opc);
-                encoder.modRm_indirectDisp0(opcode.modrm_ext, ops.reg1.lowId());
-                encoder.imm32(imm);
-                break :blk;
+                const imm = emit.mir.instructions.items(.data)[inst].imm;
+                return lowerToMiEnc(tag, RegisterOrMemory.mem(ops.reg1, 0), imm, emit.code);
             }
             // mov [reg1 + imm32], reg2
             // MR
-            // We use size of source register reg2 to work out which
-            // variant of memory ptr to pick:
-            // * reg2 is 64bit - qword ptr
-            // * reg2 is 32bit - dword ptr
-            // * reg2 is 16bit - word ptr
-            // * reg2 is 8bit - byte ptr
-            const imm = mir_instructions.items(.data)[inst].imm;
-            const opcode = getArithOpCode(tag, .mr);
-            const opc: u8 = if (ops.reg2.size() == 8) opcode.opc - 1 else opcode.opc;
-            const encoder = try Encoder.init(code, 5);
-            if (ops.reg2.size() == 16) {
-                encoder.opcode_1byte(0x66);
-            }
-            encoder.rex(.{
-                .w = ops.reg2.size() == 64,
-                .r = ops.reg2.isExtended(),
-                .b = ops.reg1.isExtended(),
-            });
-            encoder.opcode_1byte(opc);
-            if (immOpSize(imm) == 8) {
-                encoder.modRm_indirectDisp8(ops.reg2.lowId(), ops.reg1.lowId());
-                encoder.disp8(@intCast(i8, imm));
-            } else {
-                encoder.modRm_indirectDisp32(ops.reg2.lowId(), ops.reg1.lowId());
-                encoder.disp32(imm);
-            }
+            const imm = emit.mir.instructions.items(.data)[inst].imm;
+            return lowerToMrEnc(tag, RegisterOrMemory.mem(ops.reg1, imm), ops.reg2, emit.code);
         },
-        0b11 => blk: {
+        0b11 => {
             if (ops.reg2 == .none) {
-                // mov [reg1 + imm32], imm32
+                // mov dword ptr [reg1 + imm32], imm32
                 // MI
-                // Base register reg1 can either be 64bit or 32bit in size.
-                // TODO for memory operand, immediate operand pair, we currently
-                // have no way of flagging whether the immediate can be 8-, 16- or
-                // 32-bit and whether the corresponding memory operand is respectively
-                // a byte, word or dword ptr.
-                // TODO we currently don't have a way to flag imm32 64bit sign extended
-                if (ops.reg1.size() != 64) {
-                    return EmitResult.err(allocator, src_loc, "size mismatch: sizeof {} != 8", .{ops.reg1});
-                }
-                const payload = mir_instructions.items(.data)[inst].payload;
-                const imm_pair = Mir.extraData(mir_extra, Mir.ImmPair, payload).data;
-                const imm_op = imm_pair.operand;
-                const opcode = getArithOpCode(tag, .mi);
-                const encoder = try Encoder.init(code, 10);
-                encoder.rex(.{
-                    .w = false,
-                    .b = ops.reg1.isExtended(),
-                });
-                encoder.opcode_1byte(opcode.opc);
-                if (immOpSize(imm_pair.dest_off) == 8) {
-                    encoder.modRm_indirectDisp8(opcode.modrm_ext, ops.reg1.lowId());
-                    encoder.disp8(@intCast(i8, imm_pair.dest_off));
-                } else {
-                    encoder.modRm_indirectDisp32(opcode.modrm_ext, ops.reg1.lowId());
-                    encoder.disp32(imm_pair.dest_off);
-                }
-                encoder.imm32(imm_op);
-                break :blk;
+                const payload = emit.mir.instructions.items(.data)[inst].payload;
+                const imm_pair = emit.mir.extraData(Mir.ImmPair, payload).data;
+                return lowerToMiEnc(
+                    tag,
+                    RegisterOrMemory.mem(ops.reg1, imm_pair.dest_off),
+                    imm_pair.operand,
+                    emit.code,
+                );
             }
-            return EmitResult.err(allocator, src_loc, "TODO unused variant: mov reg1, reg2, 0b11", .{});
+            return emit.fail("TODO unused variant: mov reg1, reg2, 0b11", .{});
         },
     }
-    return EmitResult.ok();
 }
 
 fn immOpSize(imm: i32) u8 {
@@ -888,7 +906,7 @@ fn mirArithScaleSrc(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerE
     const ops = Mir.Ops.decode(emit.mir.instructions.items(.ops)[inst]);
     const scale = ops.flags;
     // OP reg1, [reg2 + scale*rcx + imm32]
-    const opcode = getArithOpCode(tag, .rm);
+    const opcode = getOpCode(tag, .rm);
     const opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
     const imm = emit.mir.instructions.items(.data)[inst].imm;
     const encoder = try Encoder.init(emit.code, 8);
@@ -916,7 +934,7 @@ fn mirArithScaleDst(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerE
 
     if (ops.reg2 == .none) {
         // OP [reg1 + scale*rax + 0], imm32
-        const opcode = getArithOpCode(tag, .mi);
+        const opcode = getOpCode(tag, .mi);
         const opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
         const encoder = try Encoder.init(emit.code, 8);
         encoder.rex(.{
@@ -937,7 +955,7 @@ fn mirArithScaleDst(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerE
     }
 
     // OP [reg1 + scale*rax + imm32], reg2
-    const opcode = getArithOpCode(tag, .mr);
+    const opcode = getOpCode(tag, .mr);
     const opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
     const encoder = try Encoder.init(emit.code, 8);
     encoder.rex(.{
@@ -961,8 +979,8 @@ fn mirArithScaleImm(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerE
     const ops = Mir.Ops.decode(emit.mir.instructions.items(.ops)[inst]);
     const scale = ops.flags;
     const payload = emit.mir.instructions.items(.data)[inst].payload;
-    const imm_pair = Mir.extraData(emit.mir.extra, Mir.ImmPair, payload).data;
-    const opcode = getArithOpCode(tag, .mi);
+    const imm_pair = emit.mir.extraData(Mir.ImmPair, payload).data;
+    const opcode = getOpCode(tag, .mi);
     const opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
     const encoder = try Encoder.init(emit.code, 2);
     encoder.rex(.{
@@ -1023,7 +1041,7 @@ fn mirMovabs(emit: *Emit, inst: Mir.Inst.Index) InnerError!void {
 
     if (is_64) {
         const payload = emit.mir.instructions.items(.data)[inst].payload;
-        const imm64 = Mir.extraData(emit.mir.extra, Mir.Imm64, payload).data;
+        const imm64 = emit.mir.extraData(Mir.Imm64, payload).data;
         encoder.imm64(imm64.decode());
     } else {
         const imm = emit.mir.instructions.items(.data)[inst].imm;
@@ -1129,7 +1147,7 @@ fn mirLeaRip(emit: *Emit, inst: Mir.Inst.Index) InnerError!void {
     const end_offset = emit.code.items.len;
     if (@truncate(u1, ops.flags) == 0b0) {
         const payload = emit.mir.instructions.items(.data)[inst].payload;
-        const imm = Mir.extraData(emit.mir.extra, Mir.Imm64, payload).data.decode();
+        const imm = emit.mir.extraData(Mir.Imm64, payload).data.decode();
         encoder.disp32(@intCast(i32, @intCast(i64, imm) - @intCast(i64, end_offset - start_offset + 4)));
     } else {
         const got_entry = emit.mir.instructions.items(.data)[inst].got_entry;
@@ -1184,7 +1202,7 @@ fn mirDbgLine(emit: *Emit, inst: Mir.Inst.Index) InnerError!void {
     const tag = emit.mir.instructions.items(.tag)[inst];
     assert(tag == .dbg_line);
     const payload = emit.mir.instructions.items(.data)[inst].payload;
-    const dbg_line_column = Mir.extraData(emit.mir.extra, Mir.DbgLineColumn, payload).data;
+    const dbg_line_column = emit.mir.extraData(Mir.DbgLineColumn, payload).data;
     try emit.dbgAdvancePCAndLine(dbg_line_column.line, dbg_line_column.column);
 }
 
@@ -1269,7 +1287,7 @@ fn mirArgDbgInfo(emit: *Emit, inst: Mir.Inst.Index) InnerError!void {
     const tag = emit.mir.instructions.items(.tag)[inst];
     assert(tag == .arg_dbg_info);
     const payload = emit.mir.instructions.items(.data)[inst].payload;
-    const arg_dbg_info = Mir.extraData(emit.mir.extra, Mir.ArgDbgInfo, payload).data;
+    const arg_dbg_info = emit.mir.extraData(Mir.ArgDbgInfo, payload).data;
     const mcv = emit.mir.function.args[arg_dbg_info.arg_index];
     try emit.genArgDbgInfo(arg_dbg_info.air_inst, mcv);
 }
@@ -1333,111 +1351,6 @@ fn addDbgInfoTypeReloc(emit: *Emit, ty: Type) !void {
     }
 }
 
-const Mock = struct {
-    const gpa = testing.allocator;
-
-    mir_instructions: std.MultiArrayList(Mir.Inst) = .{},
-    mir_extra: std.ArrayList(u32),
-    code: std.ArrayList(u8),
-
-    fn init() Mock {
-        return .{
-            .mir_extra = std.ArrayList(u32).init(gpa),
-            .code = std.ArrayList(u8).init(gpa),
-        };
-    }
-
-    fn deinit(self: *Mock) void {
-        self.mir_instructions.deinit(gpa);
-        self.mir_extra.deinit();
-        self.code.deinit();
-    }
-
-    fn addInst(self: *Mock, inst: Mir.Inst) error{OutOfMemory}!Mir.Inst.Index {
-        try self.mir_instructions.ensureUnusedCapacity(gpa, 1);
-        const result_index = @intCast(Air.Inst.Index, self.mir_instructions.len);
-        self.mir_instructions.appendAssumeCapacity(inst);
-        return result_index;
-    }
-
-    fn addExtra(self: *Mock, extra: anytype) Allocator.Error!u32 {
-        const fields = std.meta.fields(@TypeOf(extra));
-        try self.mir_extra.ensureUnusedCapacity(fields.len);
-        return self.addExtraAssumeCapacity(extra);
-    }
-
-    fn addExtraAssumeCapacity(self: *Mock, extra: anytype) u32 {
-        const fields = std.meta.fields(@TypeOf(extra));
-        const result = @intCast(u32, self.mir_extra.items.len);
-        inline for (fields) |field| {
-            self.mir_extra.appendAssumeCapacity(switch (field.field_type) {
-                u32 => @field(extra, field.name),
-                i32 => @bitCast(u32, @field(extra, field.name)),
-                else => @compileError("bad field type"),
-            });
-        }
-        return result;
-    }
-
-    fn dummySrcLoc() Module.SrcLoc {
-        return .{
-            .file_scope = undefined,
-            .parent_decl_node = 0,
-            .lazy = .unneeded,
-        };
-    }
-
-    fn testEmitSingleSuccess(
-        self: *Mock,
-        mir_inst: Mir.Inst,
-        expected_enc: []const u8,
-        assembly: []const u8,
-    ) !void {
-        const dummy_src_loc = Mock.dummySrcLoc();
-        const code_index = self.code.items.len;
-        const mir_index = try self.addInst(mir_inst);
-        const res = switch (mir_inst.tag) {
-            .adc, .add, .sub, .xor, .@"and", .@"or", .sbb, .cmp, .mov => try mirArithImpl(
-                testing.allocator,
-                mir_inst.tag,
-                self.mir_instructions.slice(),
-                self.mir_extra.items,
-                mir_index,
-                dummy_src_loc,
-                &self.code,
-            ),
-            else => unreachable,
-        };
-        defer res.deinit(testing.allocator);
-        try testing.expect(res == .ok);
-        const code_len = if (self.code.items[code_index..].len >= expected_enc.len)
-            expected_enc.len
-        else
-            self.code.items.len - code_index;
-        try expectEqualHexStrings(expected_enc, self.code.items[code_index..][0..code_len], assembly);
-    }
-
-    fn testEmitSingleFail(self: *Mock, mir_inst: Mir.Inst, msg: []const u8) !void {
-        const dummy_src_loc = Mock.dummySrcLoc();
-        const index = try self.addInst(mir_inst);
-        const res = switch (mir_inst.tag) {
-            .adc, .add, .sub, .xor, .@"and", .@"or", .sbb, .cmp, .mov => try mirArithImpl(
-                testing.allocator,
-                mir_inst.tag,
-                self.mir_instructions.slice(),
-                self.mir_extra.items,
-                index,
-                dummy_src_loc,
-                &self.code,
-            ),
-            else => unreachable,
-        };
-        defer res.deinit(testing.allocator);
-        try testing.expect(res == .err);
-        try testing.expectEqualStrings(msg, res.err.msg);
-    }
-};
-
 fn expectEqualHexStrings(expected: []const u8, given: []const u8, assembly: []const u8) !void {
     assert(expected.len > 0);
     if (mem.eql(u8, expected, given)) return;
@@ -1458,334 +1371,118 @@ fn expectEqualHexStrings(expected: []const u8, given: []const u8, assembly: []co
     return error.TestFailed;
 }
 
-test "ARITH_OP/MOV dst_reg, src_reg" {
-    var mock = Mock.init();
-    defer mock.deinit();
-    inline for (&[_]Mir.Inst.Tag{ .adc, .add, .sub, .xor, .@"and", .@"or", .sbb, .cmp, .mov }) |tag| {
-        const opcode = comptime getArithOpCode(tag, .mr);
-        const opc = [1]u8{opcode.opc};
-        const opc_1 = [1]u8{opcode.opc - 1};
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rbp, .reg2 = .rsp }).encode(),
-            .data = undefined,
-        }, "\x48" ++ opc ++ "\xe5", @tagName(tag) ++ " rbp, rsp");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r12, .reg2 = .rax }).encode(),
-            .data = undefined,
-        }, "\x49" ++ opc ++ "\xc4", @tagName(tag) ++ " r12, rax");
-        try mock.testEmitSingleFail(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r12, .reg2 = .eax }).encode(),
-            .data = undefined,
-        }, "size mismatch: sizeof Register.r12 != sizeof Register.eax");
-        try mock.testEmitSingleFail(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r12d, .reg2 = .rax }).encode(),
-            .data = undefined,
-        }, "size mismatch: sizeof Register.r12d != sizeof Register.rax");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r12d, .reg2 = .eax }).encode(),
-            .data = undefined,
-        }, "\x41" ++ opc ++ "\xc4", @tagName(tag) ++ " r12d, eax");
-        // TODO mov r12b, ah requires a codepath without REX prefix
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r12b, .reg2 = .al }).encode(),
-            .data = undefined,
-        }, "\x41" ++ opc_1 ++ "\xc4", @tagName(tag) ++ " r12b, al");
-    }
-}
-
-test "ARITH_OP/MOV dst_reg, imm" {
-    var mock = Mock.init();
-    defer mock.deinit();
-
-    const ModRmByte = struct {
-        inline fn get(tag: Mir.Inst.Tag, reg: u8) [1]u8 {
-            const modrm: u8 = getArithOpCode(tag, .mi).modrm_ext;
-            return .{0xc0 + (modrm << 3) + reg};
-        }
-    };
+const TestEmitCode = struct {
+    buf: std.ArrayList(u8),
+    next: usize = 0,
 
-    inline for (&[_]Mir.Inst.Tag{ .adc, .add, .sub, .xor, .@"and", .@"or", .sbb, .cmp, .mov }) |tag| {
-        const opcode = comptime getArithOpCode(tag, .mi);
-        const opc = [1]u8{opcode.opc};
-        const opc_1 = [1]u8{opcode.opc - 1};
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rcx }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x48" ++ opc ++ ModRmByte.get(tag, 1) ++ "\x10\x00\x00\x00", @tagName(tag) ++ " rcx, 0x10");
-        // TODO we are wasting one byte here: this could be encoded as OI with the encoding
-        // opc + rd, imm8/16/32
-        // b9 10 00 00 00
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .ecx }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, opc ++ ModRmByte.get(tag, 1) ++ "\x10\x00\x00\x00", @tagName(tag) ++ " ecx, 0x10");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .cx }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x66" ++ opc ++ ModRmByte.get(tag, 1) ++ "\x10\x00", @tagName(tag) ++ " cx, 0x10");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r11w }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x66\x41" ++ opc ++ ModRmByte.get(tag, 3) ++ "\x10\x00", @tagName(tag) ++ " r11w, 0x10");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .cl }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, opc_1 ++ ModRmByte.get(tag, 1) ++ "\x10", @tagName(tag) ++ " cl, 0x10");
-        try mock.testEmitSingleFail(.{
-            .tag = .mov,
-            .ops = (Mir.Ops{ .reg1 = .cx }).encode(),
-            .data = .{ .imm = 0x10000000 },
-        }, "size mismatch: sizeof Register.cx != sizeof 0x10000000");
-        try mock.testEmitSingleFail(.{
-            .tag = .mov,
-            .ops = (Mir.Ops{ .reg1 = .cl }).encode(),
-            .data = .{ .imm = 0x1000 },
-        }, "size mismatch: sizeof Register.cl != sizeof 0x1000");
+    fn init() TestEmitCode {
+        return .{
+            .buf = std.ArrayList(u8).init(testing.allocator),
+        };
     }
-}
 
-test "ARITH_OP/MOV dst_reg, [imm32]" {
-    var mock = Mock.init();
-    defer mock.deinit();
-    inline for (&[_]Mir.Inst.Tag{ .adc, .add, .sub, .xor, .@"and", .@"or", .sbb, .cmp, .mov }) |tag| {
-        const opcode = comptime getArithOpCode(tag, .rm);
-        const opc = [1]u8{opcode.opc};
-        const opc_1 = [1]u8{opcode.opc - 1};
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rcx, .flags = 0b01 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x48" ++ opc ++ "\x0C\x25\x10\x00\x00\x00", @tagName(tag) ++ " rcx, [0x10]");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r11, .flags = 0b01 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x4C" ++ opc ++ "\x1C\x25\x10\x00\x00\x00", @tagName(tag) ++ " r11, [0x10]");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r11d, .flags = 0b01 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x44" ++ opc ++ "\x1C\x25\x10\x00\x00\x00", @tagName(tag) ++ " r11d, [0x10]");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r11w, .flags = 0b01 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x66\x44" ++ opc ++ "\x1C\x25\x10\x00\x00\x00", @tagName(tag) ++ " r11w, [0x10]");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r11b, .flags = 0b01 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x44" ++ opc_1 ++ "\x1C\x25\x10\x00\x00\x00", @tagName(tag) ++ " r11b, [0x10]");
+    fn deinit(emit: *TestEmitCode) void {
+        emit.buf.deinit();
+        emit.next = undefined;
     }
-}
 
-test "ARITH_OP/MOV dst_reg, [src_reg + imm]" {
-    var mock = Mock.init();
-    defer mock.deinit();
-    inline for (&[_]Mir.Inst.Tag{ .adc, .add, .sub, .xor, .@"and", .@"or", .sbb, .cmp, .mov }) |tag| {
-        const opcode = comptime getArithOpCode(tag, .rm);
-        const opc = [1]u8{opcode.opc};
-        const opc_1 = [1]u8{opcode.opc - 1};
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rcx, .reg2 = .rbp, .flags = 0b01 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x48" ++ opc ++ "\x4D\x10", @tagName(tag) ++ " rcx, [rbp + 0x10]");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rcx, .reg2 = .rbp, .flags = 0b01 }).encode(),
-            .data = .{ .imm = 0x10000000 },
-        }, "\x48" ++ opc ++ "\x8D\x00\x00\x00\x10", @tagName(tag) ++ " rcx, [rbp + 0x10000000]");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r11b, .reg2 = .rbp, .flags = 0b01 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x44" ++ opc_1 ++ "\x5D\x10", @tagName(tag) ++ " r11b, [rbp + 0x10]");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r11w, .reg2 = .rbp, .flags = 0b01 }).encode(),
-            .data = .{ .imm = 0x10000000 },
-        }, "\x66\x44" ++ opc ++ "\x9D\x00\x00\x00\x10", @tagName(tag) ++ " r11w, [rbp + 0x10000000]");
+    fn buffer(emit: *TestEmitCode) *std.ArrayList(u8) {
+        emit.next = emit.buf.items.len;
+        return &emit.buf;
     }
-}
-
-test "ARITH_OP/MOV [dst_reg + 0], imm" {
-    var mock = Mock.init();
-    defer mock.deinit();
-
-    const ModRmByte = struct {
-        inline fn get(tag: Mir.Inst.Tag, reg: u8) [1]u8 {
-            const modrm: u8 = getArithOpCode(tag, .mi).modrm_ext;
-            return .{(modrm << 3) + reg};
-        }
-    };
 
-    inline for (&[_]Mir.Inst.Tag{ .adc, .add, .sub, .xor, .@"and", .@"or", .sbb, .cmp, .mov }) |tag| {
-        const opcode = comptime getArithOpCode(tag, .mi);
-        const opc = [1]u8{opcode.opc};
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r11, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x41" ++ opc ++ ModRmByte.get(tag, 3) ++ "\x10\x00\x00\x00", @tagName(tag) ++ " dword ptr [r11 + 0], 0x10");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rax, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x10000000 },
-        }, opc ++ ModRmByte.get(tag, 0) ++ "\x00\x00\x00\x10", @tagName(tag) ++ " dword ptr [rax + 0], 0x10000000");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rax, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x1000 },
-        }, opc ++ ModRmByte.get(tag, 0) ++ "\x00\x10\x00\x00", @tagName(tag) ++ " dword ptr [rax + 0], 0x1000");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rax, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, opc ++ ModRmByte.get(tag, 0) ++ "\x10\x00\x00\x00", @tagName(tag) ++ " dword ptr [rax + 0], 0x10");
-        try mock.testEmitSingleFail(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .eax, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "size mismatch: sizeof Register.eax != 8");
+    fn emitted(emit: TestEmitCode) []const u8 {
+        return emit.buf.items[emit.next..];
     }
-}
+};
 
-test "ARITH_OP/MOV [dst_reg + imm32], src_reg" {
-    var mock = Mock.init();
-    defer mock.deinit();
-    inline for (&[_]Mir.Inst.Tag{ .adc, .add, .sub, .xor, .@"and", .@"or", .sbb, .cmp, .mov }) |tag| {
-        const opcode = comptime getArithOpCode(tag, .mr);
-        const opc = [1]u8{opcode.opc};
-        const opc_1 = [1]u8{opcode.opc - 1};
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rbp, .reg2 = .r11, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x4c" ++ opc ++ "\x5d\x10", @tagName(tag) ++ " qword ptr [rbp + 0x10], r11");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rbp, .reg2 = .r11d, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x44" ++ opc ++ "\x5d\x10", @tagName(tag) ++ " dword ptr [rbp + 0x10], r11d");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rbp, .reg2 = .r11w, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x66\x44" ++ opc ++ "\x5d\x10", @tagName(tag) ++ " word ptr [rbp + 0x10], r11w");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .rbp, .reg2 = .r11b, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x44" ++ opc_1 ++ "\x5d\x10", @tagName(tag) ++ " byte ptr [rbp + 0x10], r11b");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r11, .reg2 = .rax, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x49" ++ opc ++ "\x43\x10", @tagName(tag) ++ " qword ptr [r11 + 0x10], rax");
-        try mock.testEmitSingleSuccess(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r11, .reg2 = .eax, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "\x41" ++ opc ++ "\x43\x10", @tagName(tag) ++ " dword ptr [r11 + 0x10], eax");
-        try mock.testEmitSingleFail(.{
-            .tag = tag,
-            .ops = (Mir.Ops{ .reg1 = .r11w, .reg2 = .ax, .flags = 0b10 }).encode(),
-            .data = .{ .imm = 0x10 },
-        }, "size mismatch: sizeof Register.r11w != 8");
-    }
+test "lower MI encoding" {
+    var code = TestEmitCode.init();
+    defer code.deinit();
+    try lowerToMiEnc(.mov, RegisterOrMemory.reg(.rax), 0x10, code.buffer());
+    try expectEqualHexStrings("\x48\xc7\xc0\x10\x00\x00\x00", code.emitted(), "mov rax, 0x10");
+    try lowerToMiEnc(.mov, RegisterOrMemory.mem(.r11, 0), 0x10, code.buffer());
+    try expectEqualHexStrings("\x41\xc7\x03\x10\x00\x00\x00", code.emitted(), "mov dword ptr [r11 + 0], 0x10");
+    try lowerToMiEnc(.add, RegisterOrMemory.mem(.rdx, -8), 0x10, code.buffer());
+    try expectEqualHexStrings("\x81\x42\xF8\x10\x00\x00\x00", code.emitted(), "add dword ptr [rdx - 8], 0x10");
+    try lowerToMiEnc(.sub, RegisterOrMemory.mem(.r11, 0x10000000), 0x10, code.buffer());
+    try expectEqualHexStrings(
+        "\x41\x81\xab\x00\x00\x00\x10\x10\x00\x00\x00",
+        code.emitted(),
+        "sub dword ptr [r11 + 0x10000000], 0x10",
+    );
+    try lowerToMiEnc(.@"and", RegisterOrMemory.mem(null, 0x10000000), 0x10, code.buffer());
+    try expectEqualHexStrings(
+        "\x81\x24\x25\x00\x00\x00\x10\x10\x00\x00\x00",
+        code.emitted(),
+        "and dword ptr [ds:0x10000000], 0x10",
+    );
+    try lowerToMiEnc(.@"and", RegisterOrMemory.mem(.r12, 0x10000000), 0x10, code.buffer());
+    try expectEqualHexStrings(
+        "\x41\x81\xA4\x24\x00\x00\x00\x10\x10\x00\x00\x00",
+        code.emitted(),
+        "and dword ptr [r12 + 0x10000000], 0x10",
+    );
 }
 
-test "ARITH_OP/MOV [dst_reg + imm32], imm32" {
-    var mock = Mock.init();
-    defer mock.deinit();
-
-    const ModRmByte = struct {
-        inline fn get(tag: Mir.Inst.Tag, disp: u2, reg: u8) [1]u8 {
-            const modrm: u8 = getArithOpCode(tag, .mi).modrm_ext;
-            return .{(@as(u8, disp) << 6) + (modrm << 3) + reg};
-        }
-    };
+test "lower RM encoding" {
+    var code = TestEmitCode.init();
+    defer code.deinit();
+    try lowerToRmEnc(.mov, .rax, RegisterOrMemory.reg(.rbx), code.buffer());
+    try expectEqualHexStrings("\x48\x8b\xc3", code.emitted(), "mov rax, rbx");
+    try lowerToRmEnc(.mov, .rax, RegisterOrMemory.mem(.r11, 0), code.buffer());
+    try expectEqualHexStrings("\x49\x8b\x03", code.emitted(), "mov rax, qword ptr [r11 + 0]");
+    try lowerToRmEnc(.add, .r11, RegisterOrMemory.mem(null, 0x10000000), code.buffer());
+    try expectEqualHexStrings(
+        "\x4C\x03\x1C\x25\x00\x00\x00\x10",
+        code.emitted(),
+        "add r11, qword ptr [ds:0x10000000]",
+    );
+    try lowerToRmEnc(.add, .r12b, RegisterOrMemory.mem(null, 0x10000000), code.buffer());
+    try expectEqualHexStrings(
+        "\x44\x02\x24\x25\x00\x00\x00\x10",
+        code.emitted(),
+        "add r11b, byte ptr [ds:0x10000000]",
+    );
+    try lowerToRmEnc(.sub, .r11, RegisterOrMemory.mem(.r13, 0x10000000), code.buffer());
+    try expectEqualHexStrings(
+        "\x4D\x2B\x9D\x00\x00\x00\x10",
+        code.emitted(),
+        "sub r11, qword ptr [r13 + 0x10000000]",
+    );
+    try lowerToRmEnc(.sub, .r11, RegisterOrMemory.mem(.r12, 0x10000000), code.buffer());
+    try expectEqualHexStrings(
+        "\x4D\x2B\x9C\x24\x00\x00\x00\x10",
+        code.emitted(),
+        "sub r11, qword ptr [r12 + 0x10000000]",
+    );
+    try lowerToRmEnc(.mov, .rax, RegisterOrMemory.mem(.rbp, -4), code.buffer());
+    try expectEqualHexStrings("\x48\x8B\x45\xFC", code.emitted(), "mov rax, qword ptr [rbp - 4]");
+}
 
-    inline for (&[_]Mir.Inst.Tag{ .adc, .add, .sub, .xor, .@"and", .@"or", .sbb, .cmp, .mov }) |tag| {
-        const opcode = comptime getArithOpCode(tag, .mi);
-        const opc = [1]u8{opcode.opc};
-        {
-            const payload = try mock.addExtra(Mir.ImmPair{
-                .dest_off = 0x10,
-                .operand = 0x20000000,
-            });
-            try mock.testEmitSingleSuccess(.{
-                .tag = tag,
-                .ops = (Mir.Ops{ .reg1 = .rbp, .flags = 0b11 }).encode(),
-                .data = .{ .payload = payload },
-            }, opc ++ ModRmByte.get(tag, 1, 5) ++ "\x10\x00\x00\x00\x20", @tagName(tag) ++ " dword ptr [rbp + 0x10], 0x20000000");
-        }
-        {
-            const payload = try mock.addExtra(Mir.ImmPair{
-                .dest_off = 0x10,
-                .operand = 0x2000,
-            });
-            try mock.testEmitSingleSuccess(.{
-                .tag = tag,
-                .ops = (Mir.Ops{ .reg1 = .rbp, .flags = 0b11 }).encode(),
-                .data = .{ .payload = payload },
-            }, opc ++ ModRmByte.get(tag, 1, 5) ++ "\x10\x00\x20\x00\x00", @tagName(tag) ++ " dword ptr [rbp + 0x10], 0x2000");
-        }
-        {
-            const payload = try mock.addExtra(Mir.ImmPair{
-                .dest_off = 0x10,
-                .operand = 0x20,
-            });
-            try mock.testEmitSingleSuccess(.{
-                .tag = tag,
-                .ops = (Mir.Ops{ .reg1 = .rbp, .flags = 0b11 }).encode(),
-                .data = .{ .payload = payload },
-            }, opc ++ ModRmByte.get(tag, 1, 5) ++ "\x10\x20\x00\x00\x00", @tagName(tag) ++ " dword ptr [rbp + 0x10], 0x20");
-        }
-        {
-            const payload = try mock.addExtra(Mir.ImmPair{
-                .dest_off = 0x10,
-                .operand = 0x20000000,
-            });
-            try mock.testEmitSingleSuccess(.{
-                .tag = tag,
-                .ops = (Mir.Ops{ .reg1 = .r11, .flags = 0b11 }).encode(),
-                .data = .{ .payload = payload },
-            }, "\x41" ++ opc ++ ModRmByte.get(tag, 1, 3) ++ "\x10\x00\x00\x00\x20", @tagName(tag) ++ " dword ptr [r11 + 0x10], 0x20000000");
-        }
-        {
-            const payload = try mock.addExtra(Mir.ImmPair{
-                .dest_off = 0x10000000,
-                .operand = 0x20000000,
-            });
-            try mock.testEmitSingleSuccess(.{
-                .tag = tag,
-                .ops = (Mir.Ops{ .reg1 = .r11, .flags = 0b11 }).encode(),
-                .data = .{ .payload = payload },
-            }, "\x41" ++ opc ++ ModRmByte.get(tag, 2, 3) ++ "\x00\x00\x00\x10\x00\x00\x00\x20", @tagName(tag) ++ " dword ptr [r11 + 0x10], 0x20000000");
-        }
-        {
-            const payload = try mock.addExtra(Mir.ImmPair{
-                .dest_off = 0x10,
-                .operand = 0x20,
-            });
-            try mock.testEmitSingleFail(.{
-                .tag = tag,
-                .ops = (Mir.Ops{ .reg1 = .r11d, .flags = 0b11 }).encode(),
-                .data = .{ .payload = payload },
-            }, "size mismatch: sizeof Register.r11d != 8");
-        }
-    }
+test "lower MR encoding" {
+    var code = TestEmitCode.init();
+    defer code.deinit();
+    try lowerToMrEnc(.mov, RegisterOrMemory.reg(.rax), .rbx, code.buffer());
+    try expectEqualHexStrings("\x48\x89\xd8", code.emitted(), "mov rax, rbx");
+    try lowerToMrEnc(.mov, RegisterOrMemory.mem(.rbp, -4), .r11, code.buffer());
+    try expectEqualHexStrings("\x4c\x89\x5d\xfc", code.emitted(), "mov qword ptr [rbp - 4], r11");
+    try lowerToMrEnc(.add, RegisterOrMemory.mem(null, 0x10000000), .r12b, code.buffer());
+    try expectEqualHexStrings(
+        "\x44\x00\x24\x25\x00\x00\x00\x10",
+        code.emitted(),
+        "add byte ptr [ds:0x10000000], r12b",
+    );
+    try lowerToMrEnc(.add, RegisterOrMemory.mem(null, 0x10000000), .r12d, code.buffer());
+    try expectEqualHexStrings(
+        "\x44\x01\x24\x25\x00\x00\x00\x10",
+        code.emitted(),
+        "add dword ptr [ds:0x10000000], r12d",
+    );
+    try lowerToMrEnc(.sub, RegisterOrMemory.mem(.r11, 0x10000000), .r12, code.buffer());
+    try expectEqualHexStrings(
+        "\x4D\x29\xA3\x00\x00\x00\x10",
+        code.emitted(),
+        "sub qword ptr [r11 + 0x10000000], r12",
+    );
 }
src/arch/x86_64/Mir.zig
@@ -380,14 +380,14 @@ pub const Ops = struct {
     }
 };
 
-pub fn extraData(mir_extra: []const u32, comptime T: type, index: usize) struct { data: T, end: usize } {
+pub fn extraData(mir: Mir, comptime T: type, index: usize) struct { data: T, end: usize } {
     const fields = std.meta.fields(T);
     var i: usize = index;
     var result: T = undefined;
     inline for (fields) |field| {
         @field(result, field.name) = switch (field.field_type) {
-            u32 => mir_extra[i],
-            i32 => @bitCast(i32, mir_extra[i]),
+            u32 => mir.extra[i],
+            i32 => @bitCast(i32, mir.extra[i]),
             else => @compileError("bad field type"),
         };
         i += 1;
src/arch/x86_64/PrintMir.zig
@@ -481,7 +481,7 @@ fn mirArith(print: *const Print, tag: Mir.Inst.Tag, inst: Mir.Inst.Index, w: any
         0b11 => {
             if (ops.reg2 == .none) {
                 const payload = print.mir.instructions.items(.data)[inst].payload;
-                const imm_pair = Mir.extraData(print.mir.extra, Mir.ImmPair, payload).data;
+                const imm_pair = print.mir.extraData(Mir.ImmPair, payload).data;
                 try w.print("[{s} + {d}], {d}", .{ @tagName(ops.reg1), imm_pair.dest_off, imm_pair.operand });
             }
             try w.writeAll("TODO");
@@ -516,7 +516,7 @@ fn mirArithScaleImm(print: *const Print, tag: Mir.Inst.Tag, inst: Mir.Inst.Index
     const ops = Mir.Ops.decode(print.mir.instructions.items(.ops)[inst]);
     const scale = ops.flags;
     const payload = print.mir.instructions.items(.data)[inst].payload;
-    const imm_pair = Mir.extraData(print.mir.extra, Mir.ImmPair, payload).data;
+    const imm_pair = print.mir.extraData(Mir.ImmPair, payload).data;
     try w.print("{s} [{s} + {d}*rcx + {d}], {d}\n", .{ @tagName(tag), @tagName(ops.reg1), scale, imm_pair.dest_off, imm_pair.operand });
 }
 
@@ -528,7 +528,7 @@ fn mirMovabs(print: *const Print, inst: Mir.Inst.Index, w: anytype) !void {
     const is_64 = ops.reg1.size() == 64;
     const imm: i128 = if (is_64) blk: {
         const payload = print.mir.instructions.items(.data)[inst].payload;
-        const imm64 = Mir.extraData(print.mir.extra, Mir.Imm64, payload).data;
+        const imm64 = print.mir.extraData(Mir.Imm64, payload).data;
         break :blk imm64.decode();
     } else print.mir.instructions.items(.data)[inst].imm;
     if (ops.flags == 0b00) {