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

  1. Load the value into a register first (ldr), then cbz(cb, reg, offset).
  2. Fold constant-to-zero tests at compile time instead of emitting cbz on an Imm.
  3. Verify rt.is_reg() before emitting when the operand comes from generic IR.
  4. 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

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


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