Skip to content

Commit 2448314

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 768a6cf commit 2448314

4 files changed

Lines changed: 120 additions & 13 deletions

File tree

zjit/src/codegen.rs

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
612612
&Insn::Send { cd, blockiseq: None, state, reason, .. } => gen_send_without_block(jit, asm, cd, &function.frame_state(state), reason),
613613
&Insn::Send { cd, blockiseq: Some(blockiseq), state, reason, .. } => gen_send(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
614614
&Insn::SendForward { cd, blockiseq, state, reason, .. } => gen_send_forward(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
615-
Insn::SendDirect { cme, iseq, recv, args, kw_bits, blockiseq, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), *kw_bits, &function.frame_state(*state), *blockiseq),
615+
&Insn::SendDirect { cme, iseq, recv, ref args, kw_bits, blockiseq, state, .. } => gen_send_iseq_direct(
616+
cb, jit, asm, cme, iseq, opnd!(recv), opnds!(args),
617+
kw_bits, &function.frame_state(state), blockiseq,
618+
),
616619
&Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
617620
&Insn::InvokeSuperForward { cd, blockiseq, state, reason, .. } => gen_invokesuperforward(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
618621
&Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason),
@@ -1652,7 +1655,7 @@ fn gen_send_iseq_direct(
16521655
};
16531656

16541657
// Make a method call. The target address will be rewritten once compiled.
1655-
let iseq_call = IseqCall::new(iseq, num_optionals_passed);
1658+
let iseq_call = IseqCall::new(iseq, num_optionals_passed.try_into().expect("checked in HIR"), args.len().try_into().expect("checked in HIR"));
16561659
let dummy_ptr = cb.get_write_ptr().raw_ptr(cb);
16571660
jit.iseq_calls.push(iseq_call.clone());
16581661
let ret = asm.ccall_with_iseq_call(dummy_ptr, c_args, &iseq_call);
@@ -2964,22 +2967,71 @@ c_callable! {
29642967
// function_stub_hit_body() may allocate and call gc_validate_pc(), so we always set PC.
29652968
let iseq_call = unsafe { Rc::from_raw(iseq_call_ptr as *const IseqCall) };
29662969
let iseq = iseq_call.iseq.get();
2970+
let argc = iseq_call.argc;
2971+
let num_opts_filled = iseq_call.jit_entry_idx;
29672972
let entry_insn_idxs = crate::hir::jit_entry_insns(iseq);
29682973
let pc = unsafe { rb_iseq_pc_at_idx(iseq, entry_insn_idxs[iseq_call.jit_entry_idx.to_usize()]) };
29692974
unsafe { rb_set_cfp_pc(cfp, pc) };
29702975

29712976
// JIT-to-JIT calls don't eagerly fill nils to non-parameter locals.
29722977
// If we side-exit from function_stub_hit (before JIT code runs), we need to set them here.
2973-
fn prepare_for_exit(iseq: IseqPtr, cfp: CfpPtr, sp: *mut VALUE, compile_error: &CompileError) {
2978+
fn prepare_for_exit(iseq: IseqPtr, cfp: CfpPtr, sp: *mut VALUE, argc: u16, num_opts_filled: u16, compile_error: &CompileError) {
29742979
unsafe {
29752980
// Set SP which gen_push_frame() doesn't set
29762981
rb_set_cfp_sp(cfp, sp);
29772982

2978-
// Fill nils to uninitialized (non-argument) locals
29792983
let local_size = get_iseq_body_local_table_size(iseq).to_usize();
2980-
let num_params = iseq.params().size.to_usize();
2981-
let base = sp.offset(-local_size_and_idx_to_bp_offset(local_size, num_params) as isize);
2982-
slice::from_raw_parts_mut(base, local_size - num_params).fill(Qnil);
2984+
let params = iseq.params();
2985+
let params_size = params.size.to_usize();
2986+
let frame_base = sp.offset(-local_size_and_idx_to_bp_offset(local_size, 0) as isize);
2987+
let locals = slice::from_raw_parts_mut(frame_base, local_size);
2988+
// Fill nils to uninitialized (non-parameter) locals
2989+
locals.get_mut(params_size..).unwrap_or_default().fill(Qnil);
2990+
2991+
// SendDirect packs args without gaps for unfilled optionals.
2992+
// When we exit to the interpreter, we need to shift args right
2993+
// to create the gap and nil-fill the unfilled optional slots.
2994+
//
2995+
// Example: def target(req, a = a, b = b, kw:); target(1, kw: 2)
2996+
// lead_num=1, opt_num=2, opts_filled=0, argc=2
2997+
//
2998+
// locals[] as placed by SendDirect (argc=2, no gaps):
2999+
// [req, kw_val, ?, ?, ?, ...]
3000+
// 0 1
3001+
// ^----caller's args----^
3002+
//
3003+
// locals[] expected by interpreter (params_size=4):
3004+
// [req, a, b, kw_val, ?, ...]
3005+
// 0 1 2 3
3006+
// ^nil ^nil^--moved--^
3007+
//
3008+
// gap_start = lead_num + opts_filled = 1
3009+
// gap_end = lead_num + opt_num = 3
3010+
// We move locals[gap_start..argc] to locals[gap_end..], then
3011+
// nil-fill locals[gap_start..gap_end].
3012+
let opt_num: usize = params.opt_num.try_into().expect("ISEQ opt_num should be non-negative");
3013+
let opts_filled = num_opts_filled.to_usize();
3014+
let opts_unfilled = opt_num.saturating_sub(opts_filled);
3015+
if opts_unfilled > 0 {
3016+
let argc = argc.to_usize();
3017+
let lead_num: usize = params.lead_num.try_into().expect("ISEQ lead_num should be non-negative");
3018+
let param_locals = &mut locals[..params_size];
3019+
// Gap of unspecified optional parameters
3020+
let gap_start = lead_num + opts_filled;
3021+
let gap_end = lead_num + opt_num;
3022+
// When there are arguments in the gap, shift them past the gap
3023+
let args_overlapping_gap = gap_start..argc;
3024+
if !args_overlapping_gap.is_empty() {
3025+
assert!(
3026+
gap_end.checked_add(args_overlapping_gap.len())
3027+
.is_some_and(|new_end| new_end <= param_locals.len()) ,
3028+
"shift past gap out-of-bounds. params={params:#?} args_overlapping_gap={args_overlapping_gap:?}"
3029+
);
3030+
param_locals.copy_within(args_overlapping_gap, gap_end);
3031+
}
3032+
// Nil-fill the now-vacant optional parameter slots
3033+
param_locals[gap_start..gap_end].fill(Qnil);
3034+
}
29833035
}
29843036

29853037
// Increment a compile error counter for --zjit-stats
@@ -3003,7 +3055,7 @@ c_callable! {
30033055
// We'll use this Rc again, so increment the ref count decremented by from_raw.
30043056
unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); }
30053057

3006-
prepare_for_exit(iseq, cfp, sp, compile_error);
3058+
prepare_for_exit(iseq, cfp, sp, argc, num_opts_filled, compile_error);
30073059
return ZJITState::get_exit_trampoline_with_counter().raw_ptr(cb);
30083060
}
30093061

@@ -3018,7 +3070,7 @@ c_callable! {
30183070
// We'll use this Rc again, so increment the ref count decremented by from_raw.
30193071
unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); }
30203072

3021-
prepare_for_exit(iseq, cfp, sp, &compile_error);
3073+
prepare_for_exit(iseq, cfp, sp, argc, num_opts_filled, &compile_error);
30223074
ZJITState::get_exit_trampoline_with_counter()
30233075
});
30243076
cb.mark_all_executable();
@@ -3343,7 +3395,10 @@ pub struct IseqCall {
33433395
pub iseq: Cell<IseqPtr>,
33443396

33453397
/// Index that corresponds to [crate::hir::jit_entry_insns]
3346-
jit_entry_idx: u32,
3398+
jit_entry_idx: u16,
3399+
3400+
/// Argument count passing to the HIR function
3401+
argc: u16,
33473402

33483403
/// Position where the call instruction starts
33493404
start_addr: Cell<Option<CodePtr>>,
@@ -3356,12 +3411,13 @@ pub type IseqCallRef = Rc<IseqCall>;
33563411

33573412
impl IseqCall {
33583413
/// Allocate a new IseqCall
3359-
fn new(iseq: IseqPtr, jit_entry_idx: u32) -> IseqCallRef {
3414+
fn new(iseq: IseqPtr, jit_entry_idx: u16, argc: u16) -> IseqCallRef {
33603415
let iseq_call = IseqCall {
33613416
iseq: Cell::new(iseq),
33623417
start_addr: Cell::new(None),
33633418
end_addr: Cell::new(None),
33643419
jit_entry_idx,
3420+
argc,
33653421
};
33663422
Rc::new(iseq_call)
33673423
}

zjit/src/hir.rs

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

2359+
// IseqCall stores num_optionals_passed and argc as u16
2360+
if u16::try_from(args.len()).is_err() {
2361+
function.set_dynamic_send_reason(send_insn, TooManyArgsForLir);
2362+
return false;
2363+
}
2364+
23592365
can_send
23602366
}
23612367

zjit/src/hir/opt_tests.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14857,6 +14857,51 @@ mod hir_opt_tests {
1485714857
");
1485814858
}
1485914859

14860+
#[test]
14861+
fn test_exit_from_function_stub_for_opt_keyword_callee() {
14862+
// We have a SendDirect to a callee that fails to compile,
14863+
// so the function stub has to take care of exiting to
14864+
// interpreter.
14865+
eval("
14866+
def target(a = binding.local_variable_get(:a), b: nil)
14867+
::RubyVM::ZJIT.induce_compile_failure!
14868+
[a, b]
14869+
end
14870+
14871+
def entry = target(b: -1)
14872+
14873+
raise 'wrong' unless [nil, -1] == entry
14874+
raise 'wrong' unless [nil, -1] == entry
14875+
");
14876+
14877+
crate::hir::tests::hir_build_tests::assert_compile_fails("target", ParseError::DirectiveInduced);
14878+
let hir = hir_string("entry");
14879+
assert!(hir.contains("SendDirect"), "{hir}");
14880+
}
14881+
14882+
#[test]
14883+
fn test_exit_from_function_stub_for_lead_opt() {
14884+
// We have a SendDirect to a callee that fails to compile,
14885+
// so the function stub has to take care of exiting to
14886+
// interpreter.
14887+
let result = eval("
14888+
def target(_required, a = a, b = b)
14889+
::RubyVM::ZJIT.induce_compile_failure!
14890+
a
14891+
end
14892+
14893+
def entry = target(1)
14894+
14895+
entry
14896+
entry
14897+
");
14898+
assert_eq!(Qnil, result);
14899+
14900+
crate::hir::tests::hir_build_tests::assert_compile_fails("target", ParseError::DirectiveInduced);
14901+
let hir = hir_string("entry");
14902+
assert!(hir.contains("SendDirect"), "{hir}");
14903+
}
14904+
1486014905
#[test]
1486114906
fn test_recompile_no_profile_send() {
1486214907
// Define a callee method and a test method that calls it

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)