Skip to content

Commit 64e1036

Browse files
XrXrk0kubun
andcommitted
ZJIT: Fn stub: Move args to create appropriate unfilled optional param gap
When a SendDirect lands in a callee that fails to compile, we need to reconstruct the interpreter state at the entry point of the callee in function_stub_hit(). SendDirect never leaves gaps in its args Vec, and consequently, it contains no nils for unspecified optional parameters. ```ruby def fail(a = a, b:) = ::RubyVM::ZJIT.induce_compile_failure! def entry = fail(b: 0) ``` ``` two views of the same two stack slots │ caller's perspective │ callee's persepctive ┌────┐ │ ┌───┐ │ ?? │ │ │ b │ ├────┤ │ ├───┤ │ -1 │ │ │ a │ └────┘ │ └───┘ (argc=1) │ ``` It's up to function_stub_hit() to create the gap (of `a` in the example) and move any non-optional arguments (`b` in the example) beyond the slice of unspecified optional parameters to their proper place according to the local table. We didn't do this movement previously so returned the wrong result in some cases. Fixes the included test cases. To keep the IseqCall struct 24 bytes while accommodating for the new argc field, I've imposed a ~65K limit on the number of arguments that codegen compiles. Should be plenty, and we already have a similar limit on VM stack size. Co-authored-by: Takashi Kokubun <takashikkbn@gmail.com>
1 parent 546e2eb commit 64e1036

File tree

4 files changed

+120
-13
lines changed

4 files changed

+120
-13
lines changed

zjit/src/codegen.rs

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
626626
&Insn::Send { cd, block: Some(BlockHandler::BlockIseq(blockiseq)), state, reason, .. } => gen_send(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
627627
&Insn::Send { cd, block: Some(BlockHandler::BlockArg), state, reason, .. } => gen_send(jit, asm, cd, std::ptr::null(), &function.frame_state(state), reason),
628628
&Insn::SendForward { cd, blockiseq, state, reason, .. } => gen_send_forward(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
629-
Insn::SendDirect { cme, iseq, recv, args, kw_bits, block, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), *kw_bits, &function.frame_state(*state), *block),
629+
&Insn::SendDirect { cme, iseq, recv, ref args, kw_bits, block, state, .. } => gen_send_iseq_direct(
630+
cb, jit, asm, cme, iseq, opnd!(recv), opnds!(args),
631+
kw_bits, &function.frame_state(state), block,
632+
),
630633
&Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
631634
&Insn::InvokeSuperForward { cd, blockiseq, state, reason, .. } => gen_invokesuperforward(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
632635
&Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason),
@@ -1669,7 +1672,7 @@ fn gen_send_iseq_direct(
16691672
};
16701673

16711674
// Make a method call. The target address will be rewritten once compiled.
1672-
let iseq_call = IseqCall::new(iseq, num_optionals_passed);
1675+
let iseq_call = IseqCall::new(iseq, num_optionals_passed.try_into().expect("checked in HIR"), args.len().try_into().expect("checked in HIR"));
16731676
let dummy_ptr = cb.get_write_ptr().raw_ptr(cb);
16741677
jit.iseq_calls.push(iseq_call.clone());
16751678
let ret = asm.ccall_with_iseq_call(dummy_ptr, c_args, &iseq_call);
@@ -3081,22 +3084,71 @@ c_callable! {
30813084
// mutability (Cell<IseqPtr>) requires exclusive access.
30823085
let iseq_call = unsafe { Rc::from_raw(iseq_call_ptr as *const IseqCall) };
30833086
let iseq = iseq_call.iseq.get();
3087+
let argc = iseq_call.argc;
3088+
let num_opts_filled = iseq_call.jit_entry_idx;
30843089

30853090
// JIT-to-JIT calls don't eagerly fill nils to non-parameter locals.
30863091
// If we side-exit from function_stub_hit (before JIT code runs), we need to set them here.
3087-
fn prepare_for_exit(iseq: IseqPtr, cfp: CfpPtr, sp: *mut VALUE, compile_error: &CompileError) {
3092+
fn prepare_for_exit(iseq: IseqPtr, cfp: CfpPtr, sp: *mut VALUE, argc: u16, num_opts_filled: u16, compile_error: &CompileError) {
30883093
unsafe {
30893094
// Caller frames are materialized by jit_exec() after the entry trampoline returns.
30903095
// The current frame's pc and iseq are already set by function_stub_hit before this point.
30913096

30923097
// Set SP which gen_push_frame() doesn't set
30933098
rb_set_cfp_sp(cfp, sp);
30943099

3095-
// Fill nils to uninitialized (non-argument) locals
30963100
let local_size = get_iseq_body_local_table_size(iseq).to_usize();
3097-
let num_params = iseq.params().size.to_usize();
3098-
let base = sp.offset(-local_size_and_idx_to_bp_offset(local_size, num_params) as isize);
3099-
slice::from_raw_parts_mut(base, local_size - num_params).fill(Qnil);
3101+
let params = iseq.params();
3102+
let params_size = params.size.to_usize();
3103+
let frame_base = sp.offset(-local_size_and_idx_to_bp_offset(local_size, 0) as isize);
3104+
let locals = slice::from_raw_parts_mut(frame_base, local_size);
3105+
// Fill nils to uninitialized (non-parameter) locals
3106+
locals.get_mut(params_size..).unwrap_or_default().fill(Qnil);
3107+
3108+
// SendDirect packs args without gaps for unfilled optionals.
3109+
// When we exit to the interpreter, we need to shift args right
3110+
// to create the gap and nil-fill the unfilled optional slots.
3111+
//
3112+
// Example: def target(req, a = a, b = b, kw:); target(1, kw: 2)
3113+
// lead_num=1, opt_num=2, opts_filled=0, argc=2
3114+
//
3115+
// locals[] as placed by SendDirect (argc=2, no gaps):
3116+
// [req, kw_val, ?, ?, ?, ...]
3117+
// 0 1
3118+
// ^----caller's args----^
3119+
//
3120+
// locals[] expected by interpreter (params_size=4):
3121+
// [req, a, b, kw_val, ?, ...]
3122+
// 0 1 2 3
3123+
// ^nil ^nil^--moved--^
3124+
//
3125+
// gap_start = lead_num + opts_filled = 1
3126+
// gap_end = lead_num + opt_num = 3
3127+
// We move locals[gap_start..argc] to locals[gap_end..], then
3128+
// nil-fill locals[gap_start..gap_end].
3129+
let opt_num: usize = params.opt_num.try_into().expect("ISEQ opt_num should be non-negative");
3130+
let opts_filled = num_opts_filled.to_usize();
3131+
let opts_unfilled = opt_num.saturating_sub(opts_filled);
3132+
if opts_unfilled > 0 {
3133+
let argc = argc.to_usize();
3134+
let lead_num: usize = params.lead_num.try_into().expect("ISEQ lead_num should be non-negative");
3135+
let param_locals = &mut locals[..params_size];
3136+
// Gap of unspecified optional parameters
3137+
let gap_start = lead_num + opts_filled;
3138+
let gap_end = lead_num + opt_num;
3139+
// When there are arguments in the gap, shift them past the gap
3140+
let args_overlapping_gap = gap_start..argc;
3141+
if !args_overlapping_gap.is_empty() {
3142+
assert!(
3143+
gap_end.checked_add(args_overlapping_gap.len())
3144+
.is_some_and(|new_end| new_end <= param_locals.len()) ,
3145+
"shift past gap out-of-bounds. params={params:#?} args_overlapping_gap={args_overlapping_gap:?}"
3146+
);
3147+
param_locals.copy_within(args_overlapping_gap, gap_end);
3148+
}
3149+
// Nil-fill the now-vacant optional parameter slots
3150+
param_locals[gap_start..gap_end].fill(Qnil);
3151+
}
31003152
}
31013153

31023154
// Increment a compile error counter for --zjit-stats
@@ -3126,7 +3178,7 @@ c_callable! {
31263178
// We'll use this Rc again, so increment the ref count decremented by from_raw.
31273179
unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); }
31283180

3129-
prepare_for_exit(iseq, cfp, sp, compile_error);
3181+
prepare_for_exit(iseq, cfp, sp, argc, num_opts_filled, compile_error);
31303182
return ZJITState::get_exit_trampoline_with_counter().raw_ptr(cb);
31313183
}
31323184

@@ -3141,7 +3193,7 @@ c_callable! {
31413193
// We'll use this Rc again, so increment the ref count decremented by from_raw.
31423194
unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); }
31433195

3144-
prepare_for_exit(iseq, cfp, sp, &compile_error);
3196+
prepare_for_exit(iseq, cfp, sp, argc, num_opts_filled, &compile_error);
31453197
ZJITState::get_exit_trampoline_with_counter()
31463198
});
31473199
cb.mark_all_executable();
@@ -3466,7 +3518,10 @@ pub struct IseqCall {
34663518
pub iseq: Cell<IseqPtr>,
34673519

34683520
/// Index that corresponds to [crate::hir::jit_entry_insns]
3469-
jit_entry_idx: u32,
3521+
jit_entry_idx: u16,
3522+
3523+
/// Argument count passing to the HIR function
3524+
argc: u16,
34703525

34713526
/// Position where the call instruction starts
34723527
start_addr: Cell<Option<CodePtr>>,
@@ -3479,12 +3534,13 @@ pub type IseqCallRef = Rc<IseqCall>;
34793534

34803535
impl IseqCall {
34813536
/// Allocate a new IseqCall
3482-
fn new(iseq: IseqPtr, jit_entry_idx: u32) -> IseqCallRef {
3537+
fn new(iseq: IseqPtr, jit_entry_idx: u16, argc: u16) -> IseqCallRef {
34833538
let iseq_call = IseqCall {
34843539
iseq: Cell::new(iseq),
34853540
start_addr: Cell::new(None),
34863541
end_addr: Cell::new(None),
34873542
jit_entry_idx,
3543+
argc,
34883544
};
34893545
Rc::new(iseq_call)
34903546
}

zjit/src/hir.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2417,6 +2417,12 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq
24172417
return false;
24182418
}
24192419

2420+
// IseqCall stores num_optionals_passed and argc as u16
2421+
if u16::try_from(args.len()).is_err() {
2422+
function.set_dynamic_send_reason(send_insn, TooManyArgsForLir);
2423+
return false;
2424+
}
2425+
24202426
can_send
24212427
}
24222428

zjit/src/hir/opt_tests.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14960,6 +14960,51 @@ mod hir_opt_tests {
1496014960
");
1496114961
}
1496214962

14963+
#[test]
14964+
fn test_exit_from_function_stub_for_opt_keyword_callee() {
14965+
// We have a SendDirect to a callee that fails to compile,
14966+
// so the function stub has to take care of exiting to
14967+
// interpreter.
14968+
eval("
14969+
def target(a = binding.local_variable_get(:a), b: nil)
14970+
::RubyVM::ZJIT.induce_compile_failure!
14971+
[a, b]
14972+
end
14973+
14974+
def entry = target(b: -1)
14975+
14976+
raise 'wrong' unless [nil, -1] == entry
14977+
raise 'wrong' unless [nil, -1] == entry
14978+
");
14979+
14980+
crate::hir::tests::hir_build_tests::assert_compile_fails("target", ParseError::DirectiveInduced);
14981+
let hir = hir_string("entry");
14982+
assert!(hir.contains("SendDirect"), "{hir}");
14983+
}
14984+
14985+
#[test]
14986+
fn test_exit_from_function_stub_for_lead_opt() {
14987+
// We have a SendDirect to a callee that fails to compile,
14988+
// so the function stub has to take care of exiting to
14989+
// interpreter.
14990+
let result = eval("
14991+
def target(_required, a = a, b = b)
14992+
::RubyVM::ZJIT.induce_compile_failure!
14993+
a
14994+
end
14995+
14996+
def entry = target(1)
14997+
14998+
entry
14999+
entry
15000+
");
15001+
assert_eq!(Qnil, result);
15002+
15003+
crate::hir::tests::hir_build_tests::assert_compile_fails("target", ParseError::DirectiveInduced);
15004+
let hir = hir_string("entry");
15005+
assert!(hir.contains("SendDirect"), "{hir}");
15006+
}
15007+
1496315008
#[test]
1496415009
fn test_recompile_no_profile_send() {
1496515010
// Test the SideExit → recompile flow: a no-profile send becomes a SideExit,

zjit/src/hir/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ mod snapshot_tests {
207207
}
208208

209209
#[cfg(test)]
210-
pub mod hir_build_tests {
210+
pub(crate) mod hir_build_tests {
211211
use super::*;
212212
use crate::options::set_call_threshold;
213213
use insta::assert_snapshot;
@@ -275,7 +275,7 @@ pub mod hir_build_tests {
275275
}
276276

277277
#[track_caller]
278-
fn assert_compile_fails(method: &str, reason: ParseError) {
278+
pub fn assert_compile_fails(method: &str, reason: ParseError) {
279279
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
280280
unsafe { crate::cruby::rb_zjit_profile_disable(iseq) };
281281
let result = iseq_to_hir(iseq);

0 commit comments

Comments
 (0)