ruby/ruby · error
Invalid operand to ret instruction.
Error message
Invalid operand to ret instruction.
What it means
zjit's arm64 ret() encodes RET to a register and accepts only A64Opnd::None (defaults to X30, the link register) or A64Opnd::Reg. Any other variant (Imm, UImm, Mem) panics because a return target must be a register on A64.
Source
Thrown at zjit/src/asm/arm64/mod.rs:1146
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);
}
/// RET - unconditionally return to a location in a register, defaults to X30
pub fn ret(cb: &mut CodeBlock, rn: A64Opnd) {
let bytes: [u8; 4] = match rn {
A64Opnd::None => Branch::ret(30).into(),
A64Opnd::Reg(reg) => Branch::ret(reg.reg_no).into(),
_ => panic!("Invalid operand to ret instruction.")
};
cb.write_bytes(&bytes);
}
/// TBNZ - test bit and branch if not zero
pub fn tbnz(cb: &mut CodeBlock, rt: A64Opnd, bit_num: A64Opnd, offset: A64Opnd) {
let bytes: [u8; 4] = match (rt, bit_num, offset) {
(A64Opnd::Reg(rt), A64Opnd::UImm(bit_num), A64Opnd::Imm(offset)) => {
TestBit::tbnz(rt.reg_no, bit_num.try_into().unwrap(), offset.try_into().unwrap()).into()
},
_ => panic!("Invalid operand combination to tbnz instruction.")
};
cb.write_bytes(&bytes);
}
/// TBZ - test bit and branch if zeroView on GitHub (pinned to 0e5b888e1c)
Solutions
- Call ret(cb, A64Opnd::None) for the normal case (returns via X30).
- Move the target address into a register first (ldr from memory, or movz/movk for constants), then ret(cb, Xn).
- Never pass a Mem or Imm operand; check with matches!(rn, A64Opnd::None | A64Opnd::Reg(_)) before emitting.
Example fix
// before: address in memory -> panic ret(cb, A64Opnd::Mem(target_slot)); // after: load target then return ldr(cb, X16, A64Opnd::Mem(target_slot)); ret(cb, X16);
Defensive patterns
Strategy: validation
Validate before calling
let ok = matches!(rn, A64Opnd::None | A64Opnd::Reg(_));
assert!(ok, "ret target must be None or a register, got {rn:?}");
ret(cb, rn); Prevention
- Load return targets from memory or materialize constants into X16/X17 (IP registers) before ret.
- Keep a single epilogue emitter so return-target handling cannot diverge per call site.
When it happens
Trigger: Calling ret(cb, rn) with A64Opnd::Imm(addr) or A64Opnd::Mem(...) — e.g. returning to an address held in memory or computed as a constant. Passing a register works: ret(cb, X0) emits RET X0; passing None emits plain RET (X30).
Common situations: Tail-call or trampoline code that computes the destination as an immediate constant; IRs whose 'return' node carries a generic operand that happens to be an immediate; switching an epilogue from a register to an address-in-memory scheme during refactoring.
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 subs instruction.
- Invalid operand combination to sxtw instruction.
AI-assisted analysis of ruby/ruby@0e5b888e1c (2026-08-21).
Data as JSON: /api/errors/017151492a1b41e1.
Report an issue: GitHub.