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
- Wrap constants as UImm: mov(cb, X0, A64Opnd::new_uimm(42)); convert i64 only after checking v >= 0.
- 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.
- Keep rd as a register operand (W/X constants); never pass Mem or None as rd.
- 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
- Never wrap constants for mov with new_imm; use new_uimm after asserting the value is non-negative.
- Pre-check bitmask encodability; for arbitrary 64-bit constants emit a movz/movk sequence (movk exists in this module) or a literal load.
- Reject Imm/Mem/None source operands in your lowering validator before they reach the emitter.
- Add tests for UImm(0) (moves from XZR) and known bitmask patterns (0xff, 0xffff, repeating masks).
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
- Invalid operand combination to eor instruction.
- Invalid operand combination to ldaddal instruction.
- Invalid operand combination to ldaxr instruction.
- Invalid operand combination to ldp instruction.
- Invalid operand combination to ldr instruction.
AI-assisted analysis of ruby/ruby@0e5b888e1c (2026-08-21).
Data as JSON: /api/errors/5d392937135fb112.
Report an issue: GitHub.