Commit 5156ccd552

Jakub Konka <kubkon@jakubkonka.com>
2021-12-20 19:38:15
stage2: merge MOV back with arith instructions
* turns out MOV and other arithmetic instructions such as ADD can naturally share the same lowering codepath (for the same variants) * there are variants that are specific to ADD, or MOV which will be implemented as standalone MIR tags * tweak Isel tests to generate corresponding test cases for all arithmetic instructions in comptime
1 parent 71c5eeb
Changed files (3)
src/arch/x86_64/CodeGen.zig
@@ -1608,18 +1608,18 @@ fn genBinMathOpMir(
                     _ = try self.addInst(.{
                         .tag = mir_tag,
                         .ops = (Mir.Ops{
-                            .reg1 = src_reg,
-                            .reg2 = dst_reg,
-                            .flags = 0b11,
+                            .reg1 = registerAlias(dst_reg, @divExact(src_reg.size(), 8)),
+                            .reg2 = src_reg,
                         }).encode(),
                         .data = undefined,
                     });
                 },
                 .immediate => |imm| {
+                    // TODO I am not quite sure why we need to set the size of the register here...
                     _ = try self.addInst(.{
                         .tag = mir_tag,
                         .ops = (Mir.Ops{
-                            .reg1 = dst_reg,
+                            .reg1 = registerAlias(dst_reg, 4),
                         }).encode(),
                         .data = .{ .imm = @intCast(i32, imm) },
                     });
@@ -1637,7 +1637,7 @@ fn genBinMathOpMir(
                         .tag = mir_tag,
                         .ops = (Mir.Ops{
                             .reg1 = registerAlias(dst_reg, @intCast(u32, abi_size)),
-                            .reg2 = registerAlias(.rbp, @intCast(u32, abi_size)),
+                            .reg2 = .rbp,
                             .flags = 0b01,
                         }).encode(),
                         .data = .{ .imm = -@intCast(i32, adj_off) },
@@ -1667,8 +1667,8 @@ fn genBinMathOpMir(
                     _ = try self.addInst(.{
                         .tag = mir_tag,
                         .ops = (Mir.Ops{
-                            .reg1 = registerAlias(src_reg, @intCast(u32, abi_size)),
-                            .reg2 = registerAlias(.rbp, @intCast(u32, abi_size)),
+                            .reg1 = .rbp,
+                            .reg2 = registerAlias(src_reg, @intCast(u32, abi_size)),
                             .flags = 0b10,
                         }).encode(),
                         .data = .{ .imm = -@intCast(i32, adj_off) },
@@ -2924,6 +2924,7 @@ fn genSetReg(self: *Self, ty: Type, reg: Register, mcv: MCValue) InnerError!void
             }
             if (x <= math.maxInt(i32)) {
                 // Next best case: if we set the lower four bytes, the upper four will be zeroed.
+                // TODO I am not quite sure why we need to set the size of the register here...
                 _ = try self.addInst(.{
                     .tag = .mov,
                     .ops = (Mir.Ops{
src/arch/x86_64/Emit.zig
@@ -93,46 +93,41 @@ pub fn emitMir(emit: *Emit) InnerError!void {
         const inst = @intCast(u32, index);
         try emit.code_offset_mapping.putNoClobber(emit.bin_file.allocator, inst, emit.code.items.len);
         switch (tag) {
-            .adc => try emit.mirArith(.adc, inst),
-            .add => try emit.mirArith(.add, inst),
-            .sub => try emit.mirArith(.sub, inst),
-            .xor => try emit.mirArith(.xor, inst),
-            .@"and" => try emit.mirArith(.@"and", inst),
-            .@"or" => try emit.mirArith(.@"or", inst),
-            .sbb => try emit.mirArith(.sbb, inst),
-            .cmp => try emit.mirArith(.cmp, inst),
-
-            .adc_scale_src => try emit.mirArithScaleSrc(.adc, inst),
-            .add_scale_src => try emit.mirArithScaleSrc(.add, inst),
-            .sub_scale_src => try emit.mirArithScaleSrc(.sub, inst),
-            .xor_scale_src => try emit.mirArithScaleSrc(.xor, inst),
-            .and_scale_src => try emit.mirArithScaleSrc(.@"and", inst),
-            .or_scale_src => try emit.mirArithScaleSrc(.@"or", inst),
-            .sbb_scale_src => try emit.mirArithScaleSrc(.sbb, inst),
-            .cmp_scale_src => try emit.mirArithScaleSrc(.cmp, inst),
-
-            .adc_scale_dst => try emit.mirArithScaleDst(.adc, inst),
-            .add_scale_dst => try emit.mirArithScaleDst(.add, inst),
-            .sub_scale_dst => try emit.mirArithScaleDst(.sub, inst),
-            .xor_scale_dst => try emit.mirArithScaleDst(.xor, inst),
-            .and_scale_dst => try emit.mirArithScaleDst(.@"and", inst),
-            .or_scale_dst => try emit.mirArithScaleDst(.@"or", inst),
-            .sbb_scale_dst => try emit.mirArithScaleDst(.sbb, inst),
-            .cmp_scale_dst => try emit.mirArithScaleDst(.cmp, inst),
-
-            .adc_scale_imm => try emit.mirArithScaleImm(.adc, inst),
-            .add_scale_imm => try emit.mirArithScaleImm(.add, inst),
-            .sub_scale_imm => try emit.mirArithScaleImm(.sub, inst),
-            .xor_scale_imm => try emit.mirArithScaleImm(.xor, inst),
-            .and_scale_imm => try emit.mirArithScaleImm(.@"and", inst),
-            .or_scale_imm => try emit.mirArithScaleImm(.@"or", inst),
-            .sbb_scale_imm => try emit.mirArithScaleImm(.sbb, inst),
-            .cmp_scale_imm => try emit.mirArithScaleImm(.cmp, inst),
-
-            .mov => try emit.mirMov(inst),
-            .mov_scale_src => try emit.mirArithScaleSrc(.mov, inst),
-            .mov_scale_dst => try emit.mirArithScaleDst(.mov, inst),
-            .mov_scale_imm => try emit.mirArithScaleImm(.mov, inst),
+            .adc, .add, .sub, .xor, .@"and", .@"or", .sbb, .cmp, .mov => try emit.mirArith(tag, inst),
+
+            .adc_scale_src,
+            .add_scale_src,
+            .sub_scale_src,
+            .xor_scale_src,
+            .and_scale_src,
+            .or_scale_src,
+            .sbb_scale_src,
+            .cmp_scale_src,
+            .mov_scale_src,
+            => try emit.mirArithScaleSrc(tag, inst),
+
+            .adc_scale_dst,
+            .add_scale_dst,
+            .sub_scale_dst,
+            .xor_scale_dst,
+            .and_scale_dst,
+            .or_scale_dst,
+            .sbb_scale_dst,
+            .cmp_scale_dst,
+            .mov_scale_dst,
+            => try emit.mirArithScaleDst(tag, inst),
+
+            .adc_scale_imm,
+            .add_scale_imm,
+            .sub_scale_imm,
+            .xor_scale_imm,
+            .and_scale_imm,
+            .or_scale_imm,
+            .sbb_scale_imm,
+            .cmp_scale_imm,
+            .mov_scale_imm,
+            => try emit.mirArithScaleImm(tag, inst),
+
             .movabs => try emit.mirMovabs(inst),
 
             .lea => try emit.mirLea(inst),
@@ -140,19 +135,19 @@ pub fn emitMir(emit: *Emit) InnerError!void {
 
             .imul_complex => try emit.mirIMulComplex(inst),
 
-            .push => try emit.mirPushPop(.push, inst),
-            .pop => try emit.mirPushPop(.pop, inst),
+            .push, .pop => try emit.mirPushPop(tag, inst),
 
-            .jmp => try emit.mirJmpCall(.jmp, inst),
-            .call => try emit.mirJmpCall(.call, inst),
+            .jmp, .call => try emit.mirJmpCall(tag, inst),
 
-            .cond_jmp_greater_less => try emit.mirCondJmp(.cond_jmp_greater_less, inst),
-            .cond_jmp_above_below => try emit.mirCondJmp(.cond_jmp_above_below, inst),
-            .cond_jmp_eq_ne => try emit.mirCondJmp(.cond_jmp_eq_ne, inst),
+            .cond_jmp_greater_less,
+            .cond_jmp_above_below,
+            .cond_jmp_eq_ne,
+            => try emit.mirCondJmp(tag, inst),
 
-            .cond_set_byte_greater_less => try emit.mirCondSetByte(.cond_set_byte_greater_less, inst),
-            .cond_set_byte_above_below => try emit.mirCondSetByte(.cond_set_byte_above_below, inst),
-            .cond_set_byte_eq_ne => try emit.mirCondSetByte(.cond_set_byte_eq_ne, inst),
+            .cond_set_byte_greater_less,
+            .cond_set_byte_above_below,
+            .cond_set_byte_eq_ne,
+            => try emit.mirCondSetByte(tag, inst),
 
             .ret => try emit.mirRet(inst),
 
@@ -598,6 +593,7 @@ inline fn getArithOpCode(tag: Mir.Inst.Tag, enc: EncType) OpCode {
             .@"or" => .{ .opc = 0x81, .modrm_ext = 0x1 },
             .sbb => .{ .opc = 0x81, .modrm_ext = 0x3 },
             .cmp => .{ .opc = 0x81, .modrm_ext = 0x7 },
+            .mov => .{ .opc = 0xc7, .modrm_ext = 0x0 },
             else => unreachable,
         },
         .mr => {
@@ -610,6 +606,7 @@ inline fn getArithOpCode(tag: Mir.Inst.Tag, enc: EncType) OpCode {
                 .@"or" => 0x09,
                 .sbb => 0x19,
                 .cmp => 0x39,
+                .mov => 0x89,
                 else => unreachable,
             };
             return .{ .opc = opc, .modrm_ext = undefined };
@@ -624,6 +621,7 @@ inline fn getArithOpCode(tag: Mir.Inst.Tag, enc: EncType) OpCode {
                 .@"or" => 0x0b,
                 .sbb => 0x1b,
                 .cmp => 0x3b,
+                .mov => 0x8b,
                 else => unreachable,
             };
             return .{ .opc = opc, .modrm_ext = undefined };
@@ -632,262 +630,9 @@ 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 ops = Mir.Ops.decode(emit.mir.instructions.items(.ops)[inst]);
-    switch (ops.flags) {
-        0b00 => blk: {
-            if (ops.reg2 == .none) {
-                // OP reg1, imm32
-                // OP r/m64, imm32
-                const imm = emit.mir.instructions.items(.data)[inst].imm;
-                const opcode = getArithOpCode(tag, .mi);
-                const encoder = try Encoder.init(emit.code, 7);
-                encoder.rex(.{
-                    .w = ops.reg1.size() == 64,
-                    .b = ops.reg1.isExtended(),
-                });
-                if (tag != .mov and imm <= math.maxInt(i8)) {
-                    encoder.opcode_1byte(opcode.opc + 2);
-                    encoder.modRm_direct(opcode.modrm_ext, ops.reg1.lowId());
-                    encoder.imm8(@intCast(i8, imm));
-                } else {
-                    encoder.opcode_1byte(opcode.opc);
-                    encoder.modRm_direct(opcode.modrm_ext, ops.reg1.lowId());
-                    encoder.imm32(imm);
-                }
-                break :blk;
-            }
-            // OP reg1, reg2
-            // OP r/m64, r64
-            const opcode = getArithOpCode(tag, .mr);
-            const opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
-            const encoder = try Encoder.init(emit.code, 3);
-            encoder.rex(.{
-                .w = ops.reg1.size() == 64 and ops.reg2.size() == 64,
-                .r = ops.reg1.isExtended(),
-                .b = ops.reg2.isExtended(),
-            });
-            encoder.opcode_1byte(opc);
-            encoder.modRm_direct(ops.reg1.lowId(), ops.reg2.lowId());
-        },
-        0b01 => blk: {
-            const imm = emit.mir.instructions.items(.data)[inst].imm;
-            const opcode = getArithOpCode(tag, .rm);
-            const opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
-            if (ops.reg2 == .none) {
-                // OP reg1, [imm32]
-                // OP r64, r/m64
-                const encoder = try Encoder.init(emit.code, 8);
-                encoder.rex(.{
-                    .w = ops.reg1.size() == 64,
-                    .b = ops.reg1.isExtended(),
-                });
-                encoder.opcode_1byte(opc);
-                encoder.modRm_SIBDisp0(ops.reg1.lowId());
-                encoder.sib_disp32();
-                encoder.disp32(imm);
-                break :blk;
-            }
-            // OP reg1, [reg2 + imm32]
-            // OP r64, r/m64
-            const encoder = try Encoder.init(emit.code, 7);
-            encoder.rex(.{
-                .w = ops.reg1.size() == 64,
-                .r = ops.reg1.isExtended(),
-                .b = ops.reg2.isExtended(),
-            });
-            encoder.opcode_1byte(opc);
-            if (imm <= math.maxInt(i8)) {
-                encoder.modRm_indirectDisp8(ops.reg1.lowId(), ops.reg2.lowId());
-                encoder.disp8(@intCast(i8, imm));
-            } else {
-                encoder.modRm_indirectDisp32(ops.reg1.lowId(), ops.reg2.lowId());
-                encoder.disp32(imm);
-            }
-        },
-        0b10 => blk: {
-            if (ops.reg2 == .none) {
-                // OP [reg1 + 0], imm32
-                // OP r/m64, imm32
-                const imm = emit.mir.instructions.items(.data)[inst].imm;
-                const opcode = getArithOpCode(tag, .mi);
-                const opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
-                const encoder = try Encoder.init(emit.code, 7);
-                encoder.rex(.{
-                    .w = ops.reg1.size() == 64,
-                    .b = ops.reg1.isExtended(),
-                });
-                encoder.opcode_1byte(opc);
-                encoder.modRm_indirectDisp0(opcode.modrm_ext, ops.reg1.lowId());
-                if (imm <= math.maxInt(i8)) {
-                    encoder.imm8(@intCast(i8, imm));
-                } else if (imm <= math.maxInt(i16)) {
-                    encoder.imm16(@intCast(i16, imm));
-                } else {
-                    encoder.imm32(imm);
-                }
-                break :blk;
-            }
-            // OP [reg1 + imm32], reg2
-            // OP r/m64, r64
-            const imm = emit.mir.instructions.items(.data)[inst].imm;
-            const opcode = getArithOpCode(tag, .mr);
-            const opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
-            const encoder = try Encoder.init(emit.code, 7);
-            encoder.rex(.{
-                .w = ops.reg2.size() == 64,
-                .r = ops.reg1.isExtended(),
-                .b = ops.reg2.isExtended(),
-            });
-            encoder.opcode_1byte(opc);
-            if (imm <= math.maxInt(i8)) {
-                encoder.modRm_indirectDisp8(ops.reg1.lowId(), ops.reg2.lowId());
-                encoder.disp8(@intCast(i8, imm));
-            } else {
-                encoder.modRm_indirectDisp32(ops.reg1.lowId(), ops.reg2.lowId());
-                encoder.disp32(imm);
-            }
-        },
-        0b11 => blk: {
-            if (ops.reg2 == .none) {
-                // OP [reg1 + imm32], imm32
-                // OP r/m64, imm32
-                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 opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
-                const encoder = try Encoder.init(emit.code, 11);
-                encoder.rex(.{
-                    .w = false,
-                    .b = ops.reg1.isExtended(),
-                });
-                encoder.opcode_1byte(opc);
-                if (imm_pair.dest_off <= math.maxInt(i8)) {
-                    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_pair.operand);
-                break :blk;
-            }
-            // TODO clearly mov doesn't belong here; for other, arithemtic ops,
-            // this is the same as 0b00.
-            const opcode = getArithOpCode(tag, if (tag == .mov) .rm else .mr);
-            const opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
-            const encoder = try Encoder.init(emit.code, 3);
-            encoder.rex(.{
-                .w = ops.reg1.size() == 64 and ops.reg2.size() == 64,
-                .r = ops.reg1.isExtended(),
-                .b = ops.reg2.isExtended(),
-            });
-            encoder.opcode_1byte(opc);
-            encoder.modRm_direct(ops.reg1.lowId(), ops.reg2.lowId());
-        },
-    }
-}
-
-fn mirArithScaleSrc(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerError!void {
-    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 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);
-    encoder.rex(.{
-        .w = ops.reg1.size() == 64,
-        .r = ops.reg1.isExtended(),
-        .b = ops.reg2.isExtended(),
-    });
-    encoder.opcode_1byte(opc);
-    if (imm <= math.maxInt(i8)) {
-        encoder.modRm_SIBDisp8(ops.reg1.lowId());
-        encoder.sib_scaleIndexBaseDisp8(scale, Register.rcx.lowId(), ops.reg2.lowId());
-        encoder.disp8(@intCast(i8, imm));
-    } else {
-        encoder.modRm_SIBDisp32(ops.reg1.lowId());
-        encoder.sib_scaleIndexBaseDisp32(scale, Register.rcx.lowId(), ops.reg2.lowId());
-        encoder.disp32(imm);
-    }
-}
-
-fn mirArithScaleDst(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerError!void {
-    const ops = Mir.Ops.decode(emit.mir.instructions.items(.ops)[inst]);
-    const scale = ops.flags;
-    const imm = emit.mir.instructions.items(.data)[inst].imm;
-
-    if (ops.reg2 == .none) {
-        // OP [reg1 + scale*rax + 0], imm32
-        const opcode = getArithOpCode(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(.{
-            .w = ops.reg1.size() == 64,
-            .b = ops.reg1.isExtended(),
-        });
-        encoder.opcode_1byte(opc);
-        encoder.modRm_SIBDisp0(opcode.modrm_ext);
-        encoder.sib_scaleIndexBase(scale, Register.rax.lowId(), ops.reg1.lowId());
-        if (imm <= math.maxInt(i8)) {
-            encoder.imm8(@intCast(i8, imm));
-        } else if (imm <= math.maxInt(i16)) {
-            encoder.imm16(@intCast(i16, imm));
-        } else {
-            encoder.imm32(imm);
-        }
-        return;
-    }
-
-    // OP [reg1 + scale*rax + imm32], reg2
-    const opcode = getArithOpCode(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(.{
-        .w = ops.reg1.size() == 64,
-        .r = ops.reg2.isExtended(),
-        .b = ops.reg1.isExtended(),
-    });
-    encoder.opcode_1byte(opc);
-    if (imm <= math.maxInt(i8)) {
-        encoder.modRm_SIBDisp8(ops.reg2.lowId());
-        encoder.sib_scaleIndexBaseDisp8(scale, Register.rax.lowId(), ops.reg1.lowId());
-        encoder.disp8(@intCast(i8, imm));
-    } else {
-        encoder.modRm_SIBDisp32(ops.reg2.lowId());
-        encoder.sib_scaleIndexBaseDisp32(scale, Register.rax.lowId(), ops.reg1.lowId());
-        encoder.disp32(imm);
-    }
-}
-
-fn mirArithScaleImm(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerError!void {
-    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 opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
-    const encoder = try Encoder.init(emit.code, 2);
-    encoder.rex(.{
-        .w = ops.reg1.size() == 64,
-        .b = ops.reg1.isExtended(),
-    });
-    encoder.opcode_1byte(opc);
-    if (imm_pair.dest_off <= math.maxInt(i8)) {
-        encoder.modRm_SIBDisp8(opcode.modrm_ext);
-        encoder.sib_scaleIndexBaseDisp8(scale, Register.rax.lowId(), ops.reg1.lowId());
-        encoder.disp8(@intCast(i8, imm_pair.dest_off));
-    } else {
-        encoder.modRm_SIBDisp32(opcode.modrm_ext);
-        encoder.sib_scaleIndexBaseDisp32(scale, Register.rax.lowId(), ops.reg1.lowId());
-        encoder.disp32(imm_pair.dest_off);
-    }
-    encoder.imm32(imm_pair.operand);
-}
-
-fn mirMov(emit: *Emit, inst: Mir.Inst.Index) InnerError!void {
-    const res = try mirMovImpl(
+    const res = try mirArithImpl(
         emit.bin_file.allocator,
+        tag,
         emit.mir.instructions,
         emit.mir.extra,
         inst,
@@ -900,8 +645,9 @@ fn mirMov(emit: *Emit, inst: Mir.Inst.Index) InnerError!void {
     }
 }
 
-fn mirMovImpl(
+fn mirArithImpl(
     allocator: Allocator,
+    tag: Mir.Inst.Tag,
     mir_instructions: std.MultiArrayList(Mir.Inst).Slice,
     mir_extra: []const u32,
     inst: Mir.Inst.Index,
@@ -915,8 +661,8 @@ fn mirMovImpl(
                 // mov reg1, imm32
                 // MI
                 const imm = mir_instructions.items(.data)[inst].imm;
-                const opc: u8 = if (ops.reg1.size() == 8) 0xc6 else 0xc7;
-                const modrm_ext: u3 = 0x0;
+                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
@@ -929,7 +675,7 @@ fn mirMovImpl(
                     .b = ops.reg1.isExtended(),
                 });
                 encoder.opcode_1byte(opc);
-                encoder.modRm_direct(modrm_ext, ops.reg1.lowId());
+                encoder.modRm_direct(opcode.modrm_ext, ops.reg1.lowId());
                 switch (ops.reg1.size()) {
                     8 => {
                         const imm8 = math.cast(i8, imm) catch {
@@ -974,7 +720,8 @@ fn mirMovImpl(
                     ops.reg2,
                 });
             }
-            const opc: u8 = if (ops.reg1.size() == 8) 0x88 else 0x89;
+            const opcode = getArithOpCode(tag, .mr);
+            const opc: u8 = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
             const encoder = try Encoder.init(code, 3);
             encoder.rex(.{
                 .w = ops.reg1.size() == 64 and ops.reg2.size() == 64,
@@ -986,7 +733,8 @@ fn mirMovImpl(
         },
         0b01 => blk: {
             const imm = mir_instructions.items(.data)[inst].imm;
-            const opc: u8 = if (ops.reg1.size() == 8) 0x8a else 0x8b;
+            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
@@ -1045,15 +793,14 @@ fn mirMovImpl(
                 // 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 opc: u8 = 0xc7;
-                const modrm_ext: u3 = 0x0;
+                const opcode = getArithOpCode(tag, .mi);
                 const encoder = try Encoder.init(code, 7);
                 encoder.rex(.{
                     .w = false,
                     .b = ops.reg1.isExtended(),
                 });
-                encoder.opcode_1byte(opc);
-                encoder.modRm_indirectDisp0(modrm_ext, ops.reg1.lowId());
+                encoder.opcode_1byte(opcode.opc);
+                encoder.modRm_indirectDisp0(opcode.modrm_ext, ops.reg1.lowId());
                 encoder.imm32(imm);
                 break :blk;
             }
@@ -1066,7 +813,8 @@ fn mirMovImpl(
             // * reg2 is 16bit - word ptr
             // * reg2 is 8bit - byte ptr
             const imm = mir_instructions.items(.data)[inst].imm;
-            const opc: u8 = if (ops.reg2.size() == 8) 0x88 else 0x89;
+            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);
@@ -1101,19 +849,18 @@ fn mirMovImpl(
                 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 opc: u8 = 0xc7;
-                const modrm_ext: u3 = 0x0;
+                const opcode = getArithOpCode(tag, .mi);
                 const encoder = try Encoder.init(code, 10);
                 encoder.rex(.{
                     .w = false,
                     .b = ops.reg1.isExtended(),
                 });
-                encoder.opcode_1byte(opc);
+                encoder.opcode_1byte(opcode.opc);
                 if (immOpSize(imm_pair.dest_off) == 8) {
-                    encoder.modRm_indirectDisp8(modrm_ext, ops.reg1.lowId());
+                    encoder.modRm_indirectDisp8(opcode.modrm_ext, ops.reg1.lowId());
                     encoder.disp8(@intCast(i8, imm_pair.dest_off));
                 } else {
-                    encoder.modRm_indirectDisp32(modrm_ext, ops.reg1.lowId());
+                    encoder.modRm_indirectDisp32(opcode.modrm_ext, ops.reg1.lowId());
                     encoder.disp32(imm_pair.dest_off);
                 }
                 encoder.imm32(imm_op);
@@ -1137,6 +884,104 @@ fn immOpSize(imm: i32) u8 {
     return 32;
 }
 
+fn mirArithScaleSrc(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerError!void {
+    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 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);
+    encoder.rex(.{
+        .w = ops.reg1.size() == 64,
+        .r = ops.reg1.isExtended(),
+        .b = ops.reg2.isExtended(),
+    });
+    encoder.opcode_1byte(opc);
+    if (imm <= math.maxInt(i8)) {
+        encoder.modRm_SIBDisp8(ops.reg1.lowId());
+        encoder.sib_scaleIndexBaseDisp8(scale, Register.rcx.lowId(), ops.reg2.lowId());
+        encoder.disp8(@intCast(i8, imm));
+    } else {
+        encoder.modRm_SIBDisp32(ops.reg1.lowId());
+        encoder.sib_scaleIndexBaseDisp32(scale, Register.rcx.lowId(), ops.reg2.lowId());
+        encoder.disp32(imm);
+    }
+}
+
+fn mirArithScaleDst(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerError!void {
+    const ops = Mir.Ops.decode(emit.mir.instructions.items(.ops)[inst]);
+    const scale = ops.flags;
+    const imm = emit.mir.instructions.items(.data)[inst].imm;
+
+    if (ops.reg2 == .none) {
+        // OP [reg1 + scale*rax + 0], imm32
+        const opcode = getArithOpCode(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(.{
+            .w = ops.reg1.size() == 64,
+            .b = ops.reg1.isExtended(),
+        });
+        encoder.opcode_1byte(opc);
+        encoder.modRm_SIBDisp0(opcode.modrm_ext);
+        encoder.sib_scaleIndexBase(scale, Register.rax.lowId(), ops.reg1.lowId());
+        if (imm <= math.maxInt(i8)) {
+            encoder.imm8(@intCast(i8, imm));
+        } else if (imm <= math.maxInt(i16)) {
+            encoder.imm16(@intCast(i16, imm));
+        } else {
+            encoder.imm32(imm);
+        }
+        return;
+    }
+
+    // OP [reg1 + scale*rax + imm32], reg2
+    const opcode = getArithOpCode(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(.{
+        .w = ops.reg1.size() == 64,
+        .r = ops.reg2.isExtended(),
+        .b = ops.reg1.isExtended(),
+    });
+    encoder.opcode_1byte(opc);
+    if (imm <= math.maxInt(i8)) {
+        encoder.modRm_SIBDisp8(ops.reg2.lowId());
+        encoder.sib_scaleIndexBaseDisp8(scale, Register.rax.lowId(), ops.reg1.lowId());
+        encoder.disp8(@intCast(i8, imm));
+    } else {
+        encoder.modRm_SIBDisp32(ops.reg2.lowId());
+        encoder.sib_scaleIndexBaseDisp32(scale, Register.rax.lowId(), ops.reg1.lowId());
+        encoder.disp32(imm);
+    }
+}
+
+fn mirArithScaleImm(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) InnerError!void {
+    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 opc = if (ops.reg1.size() == 8) opcode.opc - 1 else opcode.opc;
+    const encoder = try Encoder.init(emit.code, 2);
+    encoder.rex(.{
+        .w = ops.reg1.size() == 64,
+        .b = ops.reg1.isExtended(),
+    });
+    encoder.opcode_1byte(opc);
+    if (imm_pair.dest_off <= math.maxInt(i8)) {
+        encoder.modRm_SIBDisp8(opcode.modrm_ext);
+        encoder.sib_scaleIndexBaseDisp8(scale, Register.rax.lowId(), ops.reg1.lowId());
+        encoder.disp8(@intCast(i8, imm_pair.dest_off));
+    } else {
+        encoder.modRm_SIBDisp32(opcode.modrm_ext);
+        encoder.sib_scaleIndexBaseDisp32(scale, Register.rax.lowId(), ops.reg1.lowId());
+        encoder.disp32(imm_pair.dest_off);
+    }
+    encoder.imm32(imm_pair.operand);
+}
+
 fn mirMovabs(emit: *Emit, inst: Mir.Inst.Index) InnerError!void {
     const tag = emit.mir.instructions.items(.tag)[inst];
     assert(tag == .movabs);
@@ -1552,8 +1397,9 @@ const Mock = struct {
         const code_index = self.code.items.len;
         const mir_index = try self.addInst(mir_inst);
         const res = switch (mir_inst.tag) {
-            .mov => try mirMovImpl(
+            .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,
@@ -1575,8 +1421,9 @@ const Mock = struct {
         const dummy_src_loc = Mock.dummySrcLoc();
         const index = try self.addInst(mir_inst);
         const res = switch (mir_inst.tag) {
-            .mov => try mirMovImpl(
+            .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,
@@ -1611,278 +1458,334 @@ fn expectEqualHexStrings(expected: []const u8, given: []const u8, assembly: []co
     return error.TestFailed;
 }
 
-test "mov dst_reg, src_reg" {
+test "ARITH_OP/MOV dst_reg, src_reg" {
     var mock = Mock.init();
     defer mock.deinit();
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rbp, .reg2 = .rsp }).encode(),
-        .data = undefined,
-    }, "\x48\x89\xe5", "mov rbp, rsp");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r12, .reg2 = .rax }).encode(),
-        .data = undefined,
-    }, "\x49\x89\xc4", "mov r12, rax");
-    try mock.testEmitSingleFail(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r12, .reg2 = .eax }).encode(),
-        .data = undefined,
-    }, "size mismatch: sizeof Register.r12 != sizeof Register.eax");
-    try mock.testEmitSingleFail(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r12d, .reg2 = .rax }).encode(),
-        .data = undefined,
-    }, "size mismatch: sizeof Register.r12d != sizeof Register.rax");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r12d, .reg2 = .eax }).encode(),
-        .data = undefined,
-    }, "\x41\x89\xc4", "mov r12d, eax");
-
-    // TODO mov r12b, ah requires a codepath without REX prefix
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r12b, .reg2 = .al }).encode(),
-        .data = undefined,
-    }, "\x41\x88\xc4", "mov r12b, al");
+    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 "mov dst_reg, imm" {
+test "ARITH_OP/MOV dst_reg, imm" {
     var mock = Mock.init();
     defer mock.deinit();
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rcx }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x48\xc7\xc1\x10\x00\x00\x00", "mov 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 = .mov,
-        .ops = (Mir.Ops{ .reg1 = .ecx }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\xc7\xc1\x10\x00\x00\x00", "mov ecx, 0x10");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .cx }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x66\xc7\xc1\x10\x00", "mov cx, 0x10");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r11w }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x66\x41\xC7\xC3\x10\x00", "mov r11w, 0x10");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .cl }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\xc6\xc1\x10", "mov 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");
-}
 
-test "mov dst_reg, [imm32]" {
-    var mock = Mock.init();
-    defer mock.deinit();
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rcx, .flags = 0b01 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x48\x8B\x0C\x25\x10\x00\x00\x00", "mov rcx, [0x10]");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r11, .flags = 0b01 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x4C\x8B\x1C\x25\x10\x00\x00\x00", "mov r11, [0x10]");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r11d, .flags = 0b01 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x44\x8B\x1C\x25\x10\x00\x00\x00", "mov r11d, [0x10]");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r11w, .flags = 0b01 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x66\x44\x8B\x1C\x25\x10\x00\x00\x00", "mov r11w, [0x10]");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r11b, .flags = 0b01 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x44\x8A\x1C\x25\x10\x00\x00\x00", "mov r11b, [0x10]");
+    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};
+        }
+    };
+
+    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");
+    }
 }
 
-test "mov dst_reg, [src_reg + imm]" {
+test "ARITH_OP/MOV dst_reg, [imm32]" {
     var mock = Mock.init();
     defer mock.deinit();
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rcx, .reg2 = .rbp, .flags = 0b01 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x48\x8B\x4D\x10", "mov rcx, [rbp + 0x10]");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rcx, .reg2 = .rbp, .flags = 0b01 }).encode(),
-        .data = .{ .imm = 0x10000000 },
-    }, "\x48\x8B\x8D\x00\x00\x00\x10", "mov rcx, [rbp + 0x10000000]");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r11b, .reg2 = .rbp, .flags = 0b01 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x44\x8A\x5D\x10", "mov r11b, [rbp + 0x10]");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r11w, .reg2 = .rbp, .flags = 0b01 }).encode(),
-        .data = .{ .imm = 0x10000000 },
-    }, "\x66\x44\x8B\x9D\x00\x00\x00\x10", "mov r11w, [rbp + 0x10000000]");
+    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]");
+    }
 }
 
-test "mov [dst_reg + 0], imm" {
+test "ARITH_OP/MOV dst_reg, [src_reg + imm]" {
     var mock = Mock.init();
     defer mock.deinit();
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r11, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x41\xC7\x03\x10\x00\x00\x00", "mov dword ptr [r11 + 0], 0x10");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rax, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x10000000 },
-    }, "\xC7\x00\x00\x00\x00\x10", "mov dword ptr [rax + 0], 0x10000000");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rax, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x1000 },
-    }, "\xC7\x00\x00\x10\x00\x00", "mov dword ptr [rax + 0], 0x1000");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rax, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\xC7\x00\x10\x00\x00\x00", "mov dword ptr [rax + 0], 0x10");
-    try mock.testEmitSingleFail(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .eax, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "size mismatch: sizeof Register.eax != 8");
+    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]");
+    }
 }
 
-test "mov [dst_reg + imm32], src_reg" {
+test "ARITH_OP/MOV [dst_reg + 0], imm" {
     var mock = Mock.init();
     defer mock.deinit();
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rbp, .reg2 = .r11, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x4c\x89\x5d\x10", "mov qword ptr [rbp + 0x10], r11");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rbp, .reg2 = .r11d, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x44\x89\x5d\x10", "mov dword ptr [rbp + 0x10], r11d");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rbp, .reg2 = .r11w, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x66\x44\x89\x5d\x10", "mov word ptr [rbp + 0x10], r11w");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .rbp, .reg2 = .r11b, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x44\x88\x5d\x10", "mov byte ptr [rbp + 0x10], r11b");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r11, .reg2 = .rax, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x49\x89\x43\x10", "mov qword ptr [r11 + 0x10], rax");
-    try mock.testEmitSingleSuccess(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r11, .reg2 = .eax, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "\x41\x89\x43\x10", "mov dword ptr [r11 + 0x10], eax");
-    try mock.testEmitSingleFail(.{
-        .tag = .mov,
-        .ops = (Mir.Ops{ .reg1 = .r11w, .reg2 = .ax, .flags = 0b10 }).encode(),
-        .data = .{ .imm = 0x10 },
-    }, "size mismatch: sizeof Register.r11w != 8");
+
+    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");
+    }
 }
 
-test "mov [dst_reg + imm32], imm32" {
+test "ARITH_OP/MOV [dst_reg + imm32], src_reg" {
     var mock = Mock.init();
     defer mock.deinit();
-    {
-        const payload = try mock.addExtra(Mir.ImmPair{
-            .dest_off = 0x10,
-            .operand = 0x20000000,
-        });
+    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 = .mov,
-            .ops = (Mir.Ops{ .reg1 = .rbp, .flags = 0b11 }).encode(),
-            .data = .{ .payload = payload },
-        }, "\xC7\x45\x10\x00\x00\x00\x20", "mov dword ptr [rbp + 0x10], 0x20000000");
-    }
-    {
-        const payload = try mock.addExtra(Mir.ImmPair{
-            .dest_off = 0x10,
-            .operand = 0x2000,
-        });
+            .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 = .mov,
-            .ops = (Mir.Ops{ .reg1 = .rbp, .flags = 0b11 }).encode(),
-            .data = .{ .payload = payload },
-        }, "\xC7\x45\x10\x00\x20\x00\x00", "mov dword ptr [rbp + 0x10], 0x2000");
-    }
-    {
-        const payload = try mock.addExtra(Mir.ImmPair{
-            .dest_off = 0x10,
-            .operand = 0x20,
-        });
+            .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 = .mov,
-            .ops = (Mir.Ops{ .reg1 = .rbp, .flags = 0b11 }).encode(),
-            .data = .{ .payload = payload },
-        }, "\xc7\x45\x10\x20\x00\x00\x00", "mov dword ptr [rbp + 0x10], 0x20");
-    }
-    {
-        const payload = try mock.addExtra(Mir.ImmPair{
-            .dest_off = 0x10,
-            .operand = 0x20000000,
-        });
+            .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 = .mov,
-            .ops = (Mir.Ops{ .reg1 = .r11, .flags = 0b11 }).encode(),
-            .data = .{ .payload = payload },
-        }, "\x41\xC7\x43\x10\x00\x00\x00\x20", "mov dword ptr [r11 + 0x10], 0x20000000");
-    }
-    {
-        const payload = try mock.addExtra(Mir.ImmPair{
-            .dest_off = 0x10000000,
-            .operand = 0x20000000,
-        });
+            .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 = .mov,
-            .ops = (Mir.Ops{ .reg1 = .r11, .flags = 0b11 }).encode(),
-            .data = .{ .payload = payload },
-        }, "\x41\xC7\x83\x00\x00\x00\x10\x00\x00\x00\x20", "mov dword ptr [r11 + 0x10], 0x20000000");
-    }
-    {
-        const payload = try mock.addExtra(Mir.ImmPair{
-            .dest_off = 0x10,
-            .operand = 0x20,
-        });
+            .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 = .mov,
-            .ops = (Mir.Ops{ .reg1 = .r11d, .flags = 0b11 }).encode(),
-            .data = .{ .payload = payload },
-        }, "size mismatch: sizeof Register.r11d != 8");
+            .tag = tag,
+            .ops = (Mir.Ops{ .reg1 = .r11w, .reg2 = .ax, .flags = 0b10 }).encode(),
+            .data = .{ .imm = 0x10 },
+        }, "size mismatch: sizeof Register.r11w != 8");
+    }
+}
+
+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};
+        }
+    };
+
+    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");
+        }
     }
 }
src/arch/x86_64/Mir.zig
@@ -136,19 +136,6 @@ pub const Inst = struct {
         cmp_scale_src,
         cmp_scale_dst,
         cmp_scale_imm,
-
-        /// ops flags:  form:
-        ///       0b00  reg1, reg2 (MR)
-        ///       0b00  reg1, imm32
-        ///       0b01  reg1, [reg2 + imm32]
-        ///       0b01  reg1, [ds:imm32]
-        ///       0b10  [reg1 + imm32], reg2
-        ///       0b10  [reg1 + 0], imm32
-        ///       0b11  [reg1 + imm32], imm32
-        ///       0b11  AVAILABLE
-        /// Notes:
-        ///  * If reg2 is `none` then it means Data field `imm` is used as the immediate.
-        ///  * When two imm32 values are required, Data field `payload` points at `ImmPair`.
         mov,
         mov_scale_src,
         mov_scale_dst,