ruby/ruby · error
Invalid operand combination to cbz instruction.
Error message
Invalid operand combination to cbz instruction.
What it means
zjit's cbz() (compare and branch on zero) takes the tested value as A64Opnd::Reg and a typed InstructionOffset. If rt is not a register (Imm, UImm, Mem, None) the function panics with this message. The offset has its own guard: the assert! at function entry rejects offsets that do not fit in 19 bits before the shape check runs.
Source
Thrown at zjit/src/asm/arm64/mod.rs:1205
} else {
imm.try_into()
}.unwrap();
LogicalImm::tst(rn.reg_no, bitmask_imm, rn.num_bits).into()
},
_ => panic!("Invalid operand combination to tst instruction."),
};
cb.write_bytes(&bytes);
}
/// CBZ - branch if a register is zero
pub fn cbz(cb: &mut CodeBlock, rt: A64Opnd, offset: InstructionOffset) {
assert!(imm_fits_bits(offset.into(), 19), "jump offset for cbz must fit in 19 bits");
let bytes: [u8; 4] = if let A64Opnd::Reg(rt) = rt {
cbz_cbnz(rt.num_bits, false, offset, rt.reg_no)
} else {
panic!("Invalid operand combination to cbz instruction.")
};
cb.write_bytes(&bytes);
}
/// CBNZ - branch if a register is non-zero
pub fn cbnz(cb: &mut CodeBlock, rt: A64Opnd, offset: InstructionOffset) {
assert!(imm_fits_bits(offset.into(), 19), "jump offset for cbz must fit in 19 bits");
let bytes: [u8; 4] = if let A64Opnd::Reg(rt) = rt {
cbz_cbnz(rt.num_bits, true, offset, rt.reg_no)
} else {
panic!("Invalid operand combination to cbnz instruction.")
};
cb.write_bytes(&bytes);
}
/// Encode Compare and Branch on Zero (CBZ) with `op=0` or Compare and Branch on Nonzero (CBNZ)View on GitHub (pinned to 0e5b888e1c)
Solutions
- Load the value into a register first (ldr), then cbz(cb, reg, offset).
- Fold constant-to-zero tests at compile time instead of emitting cbz on an Imm.
- Verify rt.is_reg() before emitting when the operand comes from generic IR.
- Keep the branch offset within 19 bits; for distant targets, branch to a nearby trampoline.
Example fix
// before: memory operand -> panic cbz(cb, A64Opnd::Mem(slot), offset); // after: load then test ldr(cb, X9, A64Opnd::Mem(slot)); cbz(cb, X9, offset);
Defensive patterns
Strategy: validation
Validate before calling
if !rt.is_reg() {
// load into a scratch register first
ldr(cb, X9, rt_as_mem);
cbz(cb, X9, offset);
} else {
cbz(cb, rt, offset);
} Prevention
- Fold 'constant == 0' tests at compile time; never emit cbz on an Imm.
- After register allocation, run a reload pass that turns any Mem test operand into a Reg.
- Keep branch offsets small (trampoline distant targets) to avoid the separate 19-bit assert.
When it happens
Trigger: Calling cbz(cb, rt, offset) where rt is a Mem operand (testing a value still in memory — arm64 has no memory-operand CBZ), an immediate (comparing a constant to zero is a compile-time fact), or None. Out-of-range offsets (>= ±2^18 instructions) fail the 19-bit imm_fits_bits assert first with a different message.
Common situations: Porting x86 'cmp [mem], 0 / je' patterns; IR lowering where a spilled virtual register is represented as a memory operand; null-check sequences on pointer values that arrive as Mem operands from the register allocator.
Related errors
- Invalid operand combination to tbnz instruction.
- Invalid operand combination to tbz instruction.
- Invalid operand combination to cbnz instruction.
- Invalid operand combination to sturh instruction: {rt:?}, {r
- Invalid operand combination to sturb instruction: {rt:?}, {r
AI-assisted analysis of ruby/ruby@0e5b888e1c (2026-08-21).
Data as JSON: /api/errors/1496bfbbb86d1ddd.
Report an issue: GitHub.