ruby/ruby · error

Invalid operand combination to tst instruction.

Error message

Invalid operand combination to tst instruction.

What it means

zjit's tst() accepts (Reg, Reg) register masks and (Reg, UImm) immediate masks; any other pair — signed Imm, Mem, None — panics here. For the UImm path the immediate must additionally be a legal A64 logical bitmask immediate (a repeating one-bit pattern); an unencodable value fails inside BitmaskImmediate::new_32b_reg(...).unwrap() with a different panic.

Source

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

/// TST - test the bits of a register against a mask, then update flags
pub fn tst(cb: &mut CodeBlock, rn: A64Opnd, rm: A64Opnd) {
    let bytes: [u8; 4] = match (rn, rm) {
        (A64Opnd::Reg(rn), A64Opnd::Reg(rm)) => {
            assert!(rn.num_bits == rm.num_bits, "All operands must be of the same size.");

            LogicalReg::tst(rn.reg_no, rm.reg_no, rn.num_bits).into()
        },
        (A64Opnd::Reg(rn), A64Opnd::UImm(imm)) => {
            let bitmask_imm = if rn.num_bits == 32 {
                BitmaskImmediate::new_32b_reg(imm.try_into().unwrap())
            } 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

View on GitHub (pinned to 0e5b888e1c)

Solutions

  1. Build the mask as unsigned: A64Opnd::new_uimm(mask).
  2. For masks that are not a valid bitmask immediate (e.g. 0x101), materialize the mask into a register and use tst(cb, Reg, Reg).
  3. Load the tested value into a register first if it is in memory.

Example fix

// before: signed Imm mask -> panic
tst(cb, W0, A64Opnd::new_imm(0xFF));

// after: unsigned mask, or register form for irregular masks
tst(cb, W0, A64Opnd::new_uimm(0xFF));
// or: movz w1, #0x101-ish irregular mask into w1, then tst(cb, W0, W1);
Defensive patterns

Strategy: validation

Validate before calling

fn can_tst(rn: &A64Opnd, rm: &A64Opnd) -> bool {
    matches!((rn, rm), (A64Opnd::Reg(_), A64Opnd::Reg(_)) | (A64Opnd::Reg(_), A64Opnd::UImm(_)))
}

Prevention

When it happens

Trigger: Calling tst(cb, rn, A64Opnd::new_imm(0xFF)) with a signed Imm instead of new_uimm(0xFF); passing a Mem operand as the mask; testing a memory operand directly. For 32-bit registers the mask must be encodable via new_32b_reg; masks like 0x80 (single bit) are fine, arbitrary values like 0x101 are not and fail the unwrap instead.

Common situations: Lowering 'x & mask == 0' checks from an IR whose constant nodes default to signed i64 (new_imm); reusing x86 'test' idioms where the mask came from a computation that produced i64 -1 (0xFFFFFFFF...) rather than u64 !0; testing flag words before they were loaded into a register.

Related errors


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