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 zero

View on GitHub (pinned to 0e5b888e1c)

Solutions

  1. Call ret(cb, A64Opnd::None) for the normal case (returns via X30).
  2. Move the target address into a register first (ldr from memory, or movz/movk for constants), then ret(cb, Xn).
  3. 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

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


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