ruby/ruby · error

attempted to lower an Opnd::Mem with a MemBase::Stack/StackI

Error message

attempted to lower an Opnd::Mem with a MemBase::Stack/StackIndirect base

What it means

Panic in `impl From<Opnd> for A64Opnd` when lowering an `Opnd::Mem` whose base is `MemBase::Stack` or `MemBase::StackIndirect`. Stack-slot references are IR-level placeholders: before emission, earlier passes must rewrite them into concrete frame-pointer (X29)-relative addresses with a computed displacement. A stack base surviving to `A64Opnd::from` means that stack-lowering pass never ran on this operand, so there is no physical address to encode.

Source

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

        self.jmp_ptr_bytes() as i32 / 4 + 1
    }
}

/// Map Opnd to A64Opnd
impl From<Opnd> for A64Opnd {
    fn from(opnd: Opnd) -> Self {
        match opnd {
            Opnd::UImm(value) => A64Opnd::new_uimm(value),
            Opnd::Imm(value) => A64Opnd::new_imm(value),
            Opnd::Reg(reg) => A64Opnd::Reg(reg),
            Opnd::Mem(Mem { base: MemBase::Reg(reg_no), num_bits, disp }) => {
                A64Opnd::new_mem(num_bits, A64Opnd::Reg(A64Reg { num_bits, reg_no }), disp)
            },
            Opnd::Mem(Mem { base: MemBase::VReg(_), .. }) => {
                panic!("attempted to lower an Opnd::Mem with a MemBase::VReg base")
            },
            Opnd::Mem(Mem { base: MemBase::Stack { .. } | MemBase::StackIndirect { .. }, .. }) => {
                panic!("attempted to lower an Opnd::Mem with a MemBase::Stack/StackIndirect base")
            },
            Opnd::VReg { .. } => panic!("attempted to lower an Opnd::VReg"),
            Opnd::Value(_) => panic!("attempted to lower an Opnd::Value"),
            Opnd::None => panic!(
                "Attempted to lower an Opnd::None. This often happens when an out operand was not allocated for an instruction because the output of the instruction was not used. Please ensure you are using the output."
            ),
        }
    }
}

/// Also implement going from a reference to an operand for convenience.
impl From<&Opnd> for A64Opnd {
    fn from(opnd: &Opnd) -> Self {
        A64Opnd::from(*opnd)
    }
}

fn emit_jmp_ptr(cb: &mut CodeBlock, dst_ptr: CodePtr) {

View on GitHub (pinned to 0e5b888e1c)

Solutions

  1. Run the pass that rewrites `MemBase::Stack`/`StackIndirect` to frame-pointer-relative `MemBase::Reg` before emission (check the pass pipeline order in the arm64 backend).
  2. If you are emitting post-regalloc code by hand, address slots via explicit X29-relative `Opnd::mem(bits, X29_OPND, offset)` built from the frame layout.
  3. When adding a new Insn kind, list its Mem operands for the stack-lowering pass so they get rewritten like existing instructions.

Example fix

// before
let slot = Opnd::Mem(Mem { num_bits: 64, base: MemBase::Stack { slot: 2 }, disp: 0 });
asm.push_insn(Insn::Load { opnd: slot, out: dst }); // panics at lowering

// after: rewrite to frame-pointer-relative address before emit
let fp = Opnd::Reg(REG_FP);
let slot = Opnd::mem(64, fp, frame_offset_of_slot(2));
asm.push_insn(Insn::Load { opnd: slot, out: dst });
Defensive patterns

Strategy: validation

Validate before calling

fn stack_bases_lowered(insns: &[Insn]) -> bool {
    insns.iter().all(|insn| insn.opnds().iter().all(|o| !matches!(o,
        Opnd::Mem(Mem { base: MemBase::Stack { .. } | MemBase::StackIndirect { .. }, .. }))))
}
assert!(stack_bases_lowered(&insns));

Type guard

fn is_fp_relative_mem(opnd: &Opnd) -> bool {
    matches!(opnd, Opnd::Mem(Mem { base: MemBase::Reg(no), .. }) if *no == FP_REG_NO)
}

Prevention

When it happens

Trigger: Emitting an instruction that references a spill slot or stack-local directly in `arm64_emit` (the emit match handles only `MemBase::Reg`); pushing a hand-built `Insn` with a Stack-based Mem into the instruction stream after the stack-slot rewriting pass; adding a new Insn variant whose operands are not visited by the stack-lowering pass.

Common situations: Extending the backend with a new instruction that takes memory operands and forgetting to route it through the stack-slot lowering pass; test harnesses that build small instruction lists by hand and call the emitter directly; refactors that reorder passes so stack rewriting happens after emit.

Related errors


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