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
- Load the memory operand into a register (ldr/ldur), then emit subs(cb, Reg, Reg, Reg).
- Use cmp(), which zjit provides as SUBS with a zero destination, for pure comparisons.
- 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
- Use zjit's cmp() for pure comparisons instead of hand-building subs with a scratch destination.
- Reload spilled (Mem) operands into registers before flags-setting instructions.
- Test comparison lowering with both register and immediate operands in CI.
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
- Invalid operand combination to sturh instruction: {rt:?}, {r
- Invalid operand combination to sturb instruction: {rt:?}, {r
- Invalid operand combination to sub instruction.
- Invalid operand combination to tst instruction.
- Invalid operand combination to eor instruction.
AI-assisted analysis of ruby/ruby@0e5b888e1c (2026-08-21).
Data as JSON: /api/errors/76adb76e4e109afd.
Report an issue: GitHub.