ruby/ruby · error

Invalid operand combination to subs instruction.

Error message

Invalid operand combination to subs instruction.

What it means

subs() is the flags-updating variant of sub in zjit's arm64 assembler and accepts the same three shapes: (Reg, Reg, Reg), (Reg, Reg, UImm), and (Reg, Reg, Imm with negative values lowered to ADDS). Any other operand combination reaches this panic. Like sub, there is no memory-operand form.

Source

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

            );

            DataReg::subs(rd.reg_no, rn.reg_no, rm.reg_no, rd.num_bits).into()
        },
        (A64Opnd::Reg(rd), A64Opnd::Reg(rn), A64Opnd::UImm(uimm12)) => {
            assert!(rd.num_bits == rn.num_bits, "rd and rn must be of the same size.");

            DataImm::subs(rd.reg_no, rn.reg_no, uimm12.try_into().unwrap(), rd.num_bits).into()
        },
        (A64Opnd::Reg(rd), A64Opnd::Reg(rn), A64Opnd::Imm(imm12)) => {
            assert!(rd.num_bits == rn.num_bits, "rd and rn must be of the same size.");

            if imm12 < 0 {
                DataImm::adds(rd.reg_no, rn.reg_no, (-imm12 as u64).try_into().unwrap(), rd.num_bits).into()
            } else {
                DataImm::subs(rd.reg_no, rn.reg_no, (imm12 as u64).try_into().unwrap(), rd.num_bits).into()
            }
        },
        _ => panic!("Invalid operand combination to subs instruction."),
    };

    cb.write_bytes(&bytes);
}

/// SXTW - sign extend a 32-bit register into a 64-bit register
pub fn sxtw(cb: &mut CodeBlock, rd: A64Opnd, rn: A64Opnd) {
    let bytes: [u8; 4] = match (rd, rn) {
        (A64Opnd::Reg(rd), A64Opnd::Reg(rn)) => {
            assert_eq!(rd.num_bits, 64, "rd must be 64-bits wide.");
            assert_eq!(rn.num_bits, 32, "rn must be 32-bits wide.");

            SBFM::sxtw(rd.reg_no, rn.reg_no).into()
        },
        _ => panic!("Invalid operand combination to sxtw instruction."),
    };

    cb.write_bytes(&bytes);

View on GitHub (pinned to 0e5b888e1c)

Solutions

  1. Load the memory operand into a register (ldr/ldur), then emit subs(cb, Reg, Reg, Reg).
  2. Use cmp(), which zjit provides as SUBS with a zero destination, for pure comparisons.
  3. For constants outside imm12 range, materialize them into a scratch register with movz/movk and use the register form.

Example fix

// before: comparing against a spilled operand -> panic
subs(cb, X0, spilled_mem_opnd, X1);

// after: reload then compare
ldr(cb, X9, spilled_mem_opnd);
subs(cb, X0, X9, X1);
Defensive patterns

Strategy: validation

Validate before calling

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

Prevention

When it happens

Trigger: Calling subs(cb, rd, rn, rm) with any Mem operand in any position, with an immediate in rn, or with A64Opnd::None. Separate asserts fire first for mismatched register widths; immediates wider than 12 bits fail the uimm12 conversion inside the matched arm rather than at this panic.

Common situations: Emitting comparison-style code (subs + b.cond) where the compared value is still in memory (spilled operand or struct field); lowering 'cmp and branch' patterns from a high-level IR where the IR allowed memory references as instruction operands.

Related errors


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