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
- Run `compile_exits` before `arm64_emit` (verify phase order in the compile pipeline).
- If adding a new branch instruction with side-exit targets, extend `compile_exits` to rewrite its targets like existing jumps.
- 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
- Keep compile_exits strictly before arm64_emit in the pipeline.
- When adding branch instructions, extend compile_exits to rewrite their side-exit targets.
- In tests, lower side exits to labels/code pointers before invoking the emitter.
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
- We should only generate Joz/Jonz with side-exit targets
- PosMarkerAtBlockEnd should have been lowered by linearize_in
- Invalid operand combination to ands instruction.
- Invalid operand combination to asr instruction: asr {rd:?},
- Invalid operand to blr instruction.
AI-assisted analysis of ruby/ruby@0e5b888e1c (2026-08-21).
Data as JSON: /api/errors/5b51e330ed3a915d.
Report an issue: GitHub.