ruby/ruby · error

Can only split memory addresses.

Error message

Can only split memory addresses.

What it means

Panic in the arm64 helper `split_memory_address`, which exists solely to rewrite a memory operand whose displacement does not fit the 9-bit unscaled-offset encoding (using `lea`/`lea_into` into a scratch register). Its match only accepts `Opnd::Mem`; any other operand variant is a caller bug — there is no address to split.

Source

Thrown at zjit/src/backend/arm64/mod.rs:253

    {
        /// When you're storing a register into a memory location or loading a
        /// memory location into a register, the displacement from the base
        /// register of the memory location must fit into 9 bits. If it doesn't,
        /// then we need to load that memory address into a register first.
        fn split_memory_address(asm: &mut Assembler, opnd: Opnd) -> Opnd {
            match opnd {
                Opnd::Mem(mem) => {
                    if mem_disp_fits_bits(mem.disp) {
                        opnd
                    } else if asm.accept_scratch_reg {
                        asm.lea_into(SCRATCH1_OPND, Opnd::Mem(Mem { num_bits: 64, ..mem }));
                        Opnd::mem(mem.num_bits, SCRATCH1_OPND, 0)
                    } else {
                        let base = asm.lea(Opnd::Mem(Mem { num_bits: 64, ..mem }));
                        Opnd::mem(mem.num_bits, base, 0)
                    }
                },
                _ => unreachable!("Can only split memory addresses.")
            }
        }

        /// Any memory operands you're sending into an Op::Load instruction need
        /// to be split in case their displacement doesn't fit into 9 bits.
        fn split_load_operand(asm: &mut Assembler, opnd: Opnd) -> Opnd {
            match opnd {
                Opnd::Reg(_) | Opnd::VReg { .. } => opnd,
                Opnd::Mem(_) => {
                    let split_opnd = split_memory_address(asm, opnd);
                    let out_opnd = asm.load(split_opnd);
                    // Many Arm insns support only 32-bit or 64-bit operands. asm.load with fewer
                    // bits zero-extends the value, so it's safe to recognize it as a 32-bit value.
                    if out_opnd.rm_num_bits() < 32 {
                        out_opnd.with_num_bits(32)
                    } else {
                        out_opnd
                    }

View on GitHub (pinned to 0e5b888e1c)

Solutions

  1. Guard the call: only invoke `split_memory_address` when `matches!(opnd, Opnd::Mem(_))`; pass other operands through unchanged.
  2. Prefer the existing wrapper `split_load_operand`, which already passes Reg/VReg through and only splits Mem operands.
  3. If a new instruction can take Mem or Reg in the same slot, mirror the `match opnd { Opnd::Mem(mem) => ..., _ => opnd }` shape used at this call site.

Example fix

// before
let opnd = split_memory_address(asm, opnd); // opnd may be Opnd::Reg -> panics

// after
let opnd = match opnd {
    Opnd::Mem(_) => split_memory_address(asm, opnd),
    other => other,
};
Defensive patterns

Strategy: type-guard

Validate before calling

let split = match opnd {
    Opnd::Mem(_) => split_memory_address(asm, opnd),
    other => other,
};

Type guard

fn is_splittable_address(opnd: &Opnd) -> bool {
    matches!(opnd, Opnd::Mem(_))
}

Prevention

When it happens

Trigger: Calling `split_memory_address(asm, opnd)` (directly or by adding a new call site) with an `Opnd::Reg`, `Opnd::UImm`, `Opnd::None`, etc. Note the sibling `split_load_operand` deliberately guards with `Opnd::Mem(_)` before calling it; new code that skips that guard hits the `unreachable!`.

Common situations: Refactoring operand splitting so non-Mem operands are routed into `split_memory_address`; writing a new instruction rule that assumes its operand is always memory without checking the IR can actually produce registers/immediates there.

Related errors


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