ruby/ruby · critical

Target::SideExit should have been compiled by compile_exits

Error message

Target::SideExit should have been compiled by compile_exits

What it means

Panic in `emit_conditional_jump` on arm64 when the branch target is still a `Target::SideExit`. The pipeline invariant is that `compile_exits` runs before final emission and rewrites every side-exit target into a concrete label or code pointer. A `SideExit` target reaching this matcher means that pass was skipped or does not cover the instruction that produced this branch.

Source

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

                // We need to make sure we have at least 6 instructions for
                // every kind of jump for invalidation purposes, so we're
                // going to write out padding nop instructions here.
                assert!(num_insns <= cb.conditional_jump_insns());
                (num_insns..cb.conditional_jump_insns()).for_each(|_| nop(cb));
            }

            let label = match target {
                Target::CodePtr(dst_ptr) => {
                    let dst_addr = dst_ptr.as_offset();
                    let src_addr = cb.get_write_ptr().as_offset();
                    generate_branch::<CONDITION>(cb, src_addr, dst_addr);
                    return;
                },
                Target::Label(l) => l,
                Target::Block(ref edge) => asm.block_label(edge.target),
                Target::SideExit(..) => {
                    unreachable!("Target::SideExit should have been compiled by compile_exits")
                },
            };
            // Try to use a single B.cond instruction
            cb.label_ref(label, 4, |cb, src_addr, dst_addr| {
                // +1 since src_addr is after the instruction while A64
                // counts the offset relative to the start.
                let offset = (dst_addr - src_addr) / 4 + 1;
                if bcond_offset_fits_bits(offset) {
                    bcond(cb, CONDITION, InstructionOffset::from_insns(offset as i32));
                    Ok(())
                } else {
                    Err(())
                }
            });
        }

        /// Emit a CBZ or CBNZ which branches when a register is zero or non-zero
        fn emit_cmp_zero_jump(cb: &mut CodeBlock, reg: A64Opnd, branch_if_zero: bool, target: Target) {

View on GitHub (pinned to 0e5b888e1c)

Solutions

  1. Run `compile_exits` before `arm64_emit` (verify phase order in the compile pipeline).
  2. If adding a new branch instruction with side-exit targets, extend `compile_exits` to rewrite its targets like existing jumps.
  3. In tests, lower side-exit targets to labels or code pointers before invoking the emitter.

Example fix

// before
let insns = build_ir_with_side_exits();
backend.arm64_emit(&insns);   // panics: SideExit not compiled

// after
let insns = build_ir_with_side_exits();
backend.compile_exits(&mut insns); // rewrites SideExit -> label/code ptr
backend.arm64_emit(&insns);
Defensive patterns

Strategy: validation

Validate before calling

fn side_exits_compiled(insns: &[Insn]) -> bool {
    insns.iter().all(|insn| insn.branch_target()
        .map(|t| !matches!(t, Target::SideExit(..))).unwrap_or(true))
}
assert!(side_exits_compiled(&linearized), "run compile_exits before emit");

Type guard

fn is_emittable_target(t: &Target) -> bool {
    !matches!(t, Target::SideExit(..))
}

Prevention

When it happens

Trigger: Emitting code containing a conditional branch to `Target::SideExit(..)` without running `compile_exits` first; adding a new branch-producing Insn whose side-exit targets are not rewritten by `compile_exits`; reordering backend phases so exit compilation happens after `arm64_emit`.

Common situations: Backend pipeline refactors or new conditional-jump instructions added during feature work; unit tests invoking the emitter on partial IR that still carries side-exit edges.

Related errors


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