ruby/ruby · error

Invalid operand combination to mov instruction: {rd:?}, {rm:

Error message

Invalid operand combination to mov instruction: {rd:?}, {rm:?}

What it means

Panic from the catch-all '_' arm of the 'mov' emitter. The matched shapes are register-to-register (including special-cased SP/XZR handling) and (Reg, UImm) for immediate moves, where UImm(0) maps to a move from XZR and other values go through BitmaskImmediate encoding. The catch-all fires when rm is a signed Imm(i64), a Mem, or None — or when rd is not a register. Note: a UImm that is not a legal ARM64 bitmask immediate panics separately inside BitmaskImmediate::new(..).unwrap().

Source

Thrown at zjit/src/asm/arm64/mod.rs:709

        },
        (A64Opnd::Reg(rd), A64Opnd::Reg(rm)) => {
            assert!(rd.num_bits == rm.num_bits, "Expected registers to be the same size");

            LogicalReg::mov(rd.reg_no, rm.reg_no, rd.num_bits).into()
        },
        (A64Opnd::Reg(rd), A64Opnd::UImm(0)) => {
            LogicalReg::mov(rd.reg_no, XZR_REG.reg_no, rd.num_bits).into()
        },
        (A64Opnd::Reg(rd), A64Opnd::UImm(imm)) => {
            let bitmask_imm = if rd.num_bits == 32 {
                BitmaskImmediate::new_32b_reg(imm.try_into().unwrap())
            } else {
                imm.try_into()
            }.unwrap();

            LogicalImm::mov(rd.reg_no, bitmask_imm, rd.num_bits).into()
        },
        _ => panic!("Invalid operand combination to mov instruction: {rd:?}, {rm:?}")
    };

    cb.write_bytes(&bytes);
}

/// MOVK - move a 16 bit immediate into a register, keep the other bits in place
pub fn movk(cb: &mut CodeBlock, rd: A64Opnd, imm16: A64Opnd, shift: u8) {
    let bytes: [u8; 4] = match (rd, imm16) {
        (A64Opnd::Reg(rd), A64Opnd::UImm(imm16)) => {
            assert!(uimm_fits_bits(imm16, 16), "The immediate operand must be 16 bits or less.");

            Mov::movk(rd.reg_no, imm16 as u16, shift, rd.num_bits).into()
        },
        _ => panic!("Invalid operand combination to movk instruction.")
    };

    cb.write_bytes(&bytes);
}

View on GitHub (pinned to 0e5b888e1c)

Solutions

  1. Wrap constants as UImm: mov(cb, X0, A64Opnd::new_uimm(42)); convert i64 only after checking v >= 0.
  2. Pre-check bitmask encodability before emitting; if the value cannot be encoded, emit movk/movz-style sequences (movk exists in this module) or use a literal-pool load.
  3. Keep rd as a register operand (W/X constants); never pass Mem or None as rd.
  4. For register-to-register moves keep both operands Reg with compatible widths (the SP/XZR special case expects 64-bit rm).

Example fix

// before
mov(cb, X0, A64Opnd::new_imm(0x30001)); // Imm not UImm; also not bitmask-encodable

// after
let v: u64 = 0x30001;
mov(cb, X0, A64Opnd::new_uimm(v & 0xffff));      // movz x0, #(v & 0xffff)
movk(cb, X0, A64Opnd::new_uimm(v >> 16), 16);    // movk x0, #(v >> 16), lsl #16
Defensive patterns

Strategy: validation

Validate before calling

fn mov_ok(rd: &A64Opnd, rm: &A64Opnd) -> bool {
    if !matches!(rd, A64Opnd::Reg(_)) { return false; }
    match rm {
        A64Opnd::Reg(_) => true,
        A64Opnd::UImm(0) => true,
        A64Opnd::UImm(v) => bitmask_encodable(*v), // reject values BitmaskImmediate cannot encode
        _ => false, // Imm, Mem, None all panic
    }
}
// bitmask_encodable: true if v == 0, all-ones per width, or a replicable pattern
// (e.g. via the same N/imms logic BitmaskImmediate uses)

Type guard

fn as_reg_or_uimm(o: &A64Opnd) -> Option<Either<A64Reg, u64>> {
    match o {
        A64Opnd::Reg(r) => Some(Either::Reg(*r)),
        A64Opnd::UImm(v) => Some(Either::Imm(*v)),
        _ => None,
    }
}

Try / catch

use std::panic::{catch_unwind, AssertUnwindSafe};
let mark = cb_len(cb);
if catch_unwind(AssertUnwindSafe(|| asm::mov(cb, rd, rm))).is_err() {
    cb_truncate(cb, mark);
    return Err(CodegenError::BadOperands("mov"));
}

Prevention

When it happens

Trigger: mov(cb, X0, A64Opnd::new_imm(42)) panics: the immediate arms match only UImm. mov(cb, X0, A64Opnd::new_mem(64, X1, 0)) or rm = None also panic. mov(cb, X0, A64Opnd::new_uimm(0x30001)) matches the shape but panics inside the bitmask unwrap because 0x30001 is not bitmask-encodable.

Common situations: The single most common hit: constants flow through IR as signed i64 and get wrapped with new_imm. Also, materialising arbitrary 64-bit constants (addresses, hashes) that no bitmask immediate can represent — these need a movz/movk sequence or literal load instead of a single mov.

Related errors


AI-assisted analysis of ruby/ruby@0e5b888e1c (2026-08-21). Data as JSON: /api/errors/5d392937135fb112. Report an issue: GitHub.