Skip to content

Commit 8abc8b4

Browse files
committed
ZJIT: Fix SendDirect callee bail by restoring caller frame
When the callee returns Qundef, pop the callee frame, restore the caller's CFP with full stack state (recv + args visible), and spill locals. The interpreter can then re-execute the Send instruction with the correct stack.
1 parent 2607d3c commit 8abc8b4

1 file changed

Lines changed: 30 additions & 4 deletions

File tree

zjit/src/codegen.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,8 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func
872872
let cfp = builder.use_var(cfp_var);
873873
let ec = builder.use_var(ec_var);
874874

875-
// Save caller PC and SP
875+
// Save caller PC and SP.
876+
// Match the LIR: save SP below the recv+args that will be consumed.
876877
let pc_val = builder.ins().iconst(cl_types::I64, state.pc as i64);
877878
builder.ins().store(MemFlags::trusted(), pc_val, cfp, Offset32::new(RUBY_OFFSET_CFP_PC));
878879
let caller_sp_offset = ((state.stack().count() - argc - 1) * SIZEOF_VALUE) as i64;
@@ -1004,11 +1005,36 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func
10041005
builder.ins().brif(is_qundef, exit_block, &[], cont_block, &[]);
10051006

10061007
// Exit block: callee returned Qundef (side-exit or compilation failure).
1007-
// ec->cfp still points to the callee frame. The callee's cfp->pc was set
1008-
// during frame setup, so the interpreter can resume from there.
1009-
// Just return Qundef to propagate the bail all the way up.
1008+
// Pop the callee frame and restore the caller's state so the
1009+
// interpreter can re-execute the Send instruction correctly.
10101010
builder.seal_block(exit_block);
10111011
builder.switch_to_block(exit_block);
1012+
// Pop callee frame: ec->cfp = callee_cfp + SIZEOF_CONTROL_FRAME
1013+
let exit_ec = builder.use_var(ec_var);
1014+
let frame_sz = builder.ins().iconst(cl_types::I64, RUBY_SIZEOF_CONTROL_FRAME as i64);
1015+
let exit_callee_cfp = builder.use_var(cfp_var);
1016+
let exit_caller_cfp = builder.ins().iadd(exit_callee_cfp, frame_sz);
1017+
builder.ins().store(MemFlags::trusted(), exit_caller_cfp, exit_ec, Offset32::new(RUBY_OFFSET_EC_CFP as i32));
1018+
// Save caller PC (the Send instruction)
1019+
builder.ins().store(MemFlags::trusted(), pc_val, exit_caller_cfp, Offset32::new(RUBY_OFFSET_CFP_PC));
1020+
// Save caller SP at full stack size so the interpreter sees recv+args
1021+
let full_sp = builder.ins().iadd_imm(sp, (state.stack().count() * SIZEOF_VALUE) as i64);
1022+
builder.ins().store(MemFlags::trusted(), full_sp, exit_caller_cfp, Offset32::new(RUBY_OFFSET_CFP_SP));
1023+
// Spill caller stack (recv + args)
1024+
for (idx, &sid) in state.stack().enumerate() {
1025+
if let Some(val) = cl_opnds[sid.0] {
1026+
let offset = (idx as i32) * SIZEOF_VALUE_I32;
1027+
builder.ins().store(MemFlags::trusted(), val, sp, Offset32::new(offset));
1028+
}
1029+
}
1030+
// Spill caller locals
1031+
for (idx, &lid) in state.locals().enumerate() {
1032+
if let Some(val) = cl_opnds[lid.0] {
1033+
let ep_offset = local_idx_to_ep_offset(iseq, idx);
1034+
let byte_offset = -(ep_offset + 1) * SIZEOF_VALUE_I32;
1035+
builder.ins().store(MemFlags::trusted(), val, sp, Offset32::new(byte_offset));
1036+
}
1037+
}
10121038
builder.ins().return_(&[qundef]);
10131039

10141040
// Continue block: restore CFP and SP

0 commit comments

Comments
 (0)