Skip to content

Commit 64f088b

Browse files
committed
ZJIT: Fn stub: Move args to create appropriate unfilled optional param gap
Fixes the included test cases. It returned uninitialized VM stack memory previously. 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.
1 parent b78a84e commit 64f088b

3 files changed

Lines changed: 91 additions & 15 deletions

File tree

zjit/src/codegen.rs

Lines changed: 42 additions & 13 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+
).ok_or(state)?,
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),
@@ -1535,7 +1538,7 @@ fn gen_send_iseq_direct(
15351538
kw_bits: u32,
15361539
state: &FrameState,
15371540
blockiseq: Option<IseqPtr>,
1538-
) -> lir::Opnd {
1541+
) -> Option<lir::Opnd> {
15391542
gen_incr_counter(asm, Counter::iseq_optimized_send_count);
15401543

15411544
let local_size = unsafe { get_iseq_body_local_table_size(iseq) }.to_usize();
@@ -1651,7 +1654,7 @@ fn gen_send_iseq_direct(
16511654
};
16521655

16531656
// Make a method call. The target address will be rewritten once compiled.
1654-
let iseq_call = IseqCall::new(iseq, num_optionals_passed);
1657+
let iseq_call = IseqCall::new(iseq, num_optionals_passed.try_into().ok()?, args.len().try_into().ok()?);
16551658
let dummy_ptr = cb.get_write_ptr().raw_ptr(cb);
16561659
jit.iseq_calls.push(iseq_call.clone());
16571660
let ret = asm.ccall_with_iseq_call(dummy_ptr, c_args, &iseq_call);
@@ -1668,7 +1671,7 @@ fn gen_send_iseq_direct(
16681671
let new_sp = asm.sub(SP, sp_offset.into());
16691672
asm.mov(SP, new_sp);
16701673

1671-
ret
1674+
Some(ret)
16721675
}
16731676

16741677
/// Compile for invokeblock
@@ -2937,22 +2940,44 @@ c_callable! {
29372940
// function_stub_hit_body() may allocate and call gc_validate_pc(), so we always set PC.
29382941
let iseq_call = unsafe { Rc::from_raw(iseq_call_ptr as *const IseqCall) };
29392942
let iseq = iseq_call.iseq.get();
2943+
let argc = iseq_call.argc;
2944+
let num_opts_filled = iseq_call.jit_entry_idx;
29402945
let entry_insn_idxs = crate::hir::jit_entry_insns(iseq);
29412946
let pc = unsafe { rb_iseq_pc_at_idx(iseq, entry_insn_idxs[iseq_call.jit_entry_idx.to_usize()]) };
29422947
unsafe { rb_set_cfp_pc(cfp, pc) };
29432948

29442949
// JIT-to-JIT calls don't eagerly fill nils to non-parameter locals.
29452950
// If we side-exit from function_stub_hit (before JIT code runs), we need to set them here.
2946-
fn prepare_for_exit(iseq: IseqPtr, cfp: CfpPtr, sp: *mut VALUE, compile_error: &CompileError) {
2951+
fn prepare_for_exit(iseq: IseqPtr, cfp: CfpPtr, sp: *mut VALUE, argc: u16, num_opts_filled: u16, compile_error: &CompileError) {
29472952
unsafe {
29482953
// Set SP which gen_push_frame() doesn't set
29492954
rb_set_cfp_sp(cfp, sp);
29502955

2951-
// Fill nils to uninitialized (non-argument) locals
29522956
let local_size = get_iseq_body_local_table_size(iseq).to_usize();
2953-
let num_params = iseq.params().size.to_usize();
2954-
let base = sp.offset(-local_size_and_idx_to_bp_offset(local_size, num_params) as isize);
2955-
slice::from_raw_parts_mut(base, local_size - num_params).fill(Qnil);
2957+
let params = iseq.params();
2958+
let params_size = params.size.to_usize();
2959+
let frame_base = sp.offset(-local_size_and_idx_to_bp_offset(local_size, 0) as isize);
2960+
let locals = slice::from_raw_parts_mut(frame_base, local_size);
2961+
// Fill nils to uninitialized (non-parameter) locals
2962+
locals.get_mut(params_size..).unwrap_or_default().fill(Qnil);
2963+
2964+
// When there are unfilled optionals
2965+
let opt_num: usize = params.opt_num.try_into().expect("ISEQ opt_num should be non-negative");
2966+
let opts_unfilled = opt_num.saturating_sub(num_opts_filled.to_usize());
2967+
if opts_unfilled > 0 {
2968+
let lead_num: usize = params.lead_num.try_into().expect("ISEQ lead_num should be non-negative");
2969+
let param_locals = &mut locals[..params_size];
2970+
let args_after_opts = lead_num + num_opts_filled.to_usize()..argc.to_usize();
2971+
let after_opts_param_idx = lead_num + opt_num;
2972+
// Move to the right args that are after the unfilled optionals
2973+
if let Some(moved_by) = after_opts_param_idx.checked_sub(args_after_opts.start) {
2974+
if after_opts_param_idx + args_after_opts.len() <= param_locals.len() {
2975+
param_locals.copy_within(args_after_opts.clone(), after_opts_param_idx);
2976+
// Nil-fill unfilled optionals
2977+
param_locals.get_mut(args_after_opts.start..args_after_opts.start + moved_by).unwrap_or_default().fill(Qnil);
2978+
}
2979+
}
2980+
}
29562981
}
29572982

29582983
// Increment a compile error counter for --zjit-stats
@@ -2976,7 +3001,7 @@ c_callable! {
29763001
// We'll use this Rc again, so increment the ref count decremented by from_raw.
29773002
unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); }
29783003

2979-
prepare_for_exit(iseq, cfp, sp, compile_error);
3004+
prepare_for_exit(iseq, cfp, sp, argc, num_opts_filled, compile_error);
29803005
return ZJITState::get_exit_trampoline_with_counter().raw_ptr(cb);
29813006
}
29823007

@@ -2991,7 +3016,7 @@ c_callable! {
29913016
// We'll use this Rc again, so increment the ref count decremented by from_raw.
29923017
unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); }
29933018

2994-
prepare_for_exit(iseq, cfp, sp, &compile_error);
3019+
prepare_for_exit(iseq, cfp, sp, argc, num_opts_filled, &compile_error);
29953020
ZJITState::get_exit_trampoline_with_counter()
29963021
});
29973022
cb.mark_all_executable();
@@ -3316,7 +3341,10 @@ pub struct IseqCall {
33163341
pub iseq: Cell<IseqPtr>,
33173342

33183343
/// Index that corresponds to [crate::hir::jit_entry_insns]
3319-
jit_entry_idx: u32,
3344+
jit_entry_idx: u16,
3345+
3346+
/// Argument count passing to the HIR function
3347+
argc: u16,
33203348

33213349
/// Position where the call instruction starts
33223350
start_addr: Cell<Option<CodePtr>>,
@@ -3329,12 +3357,13 @@ pub type IseqCallRef = Rc<IseqCall>;
33293357

33303358
impl IseqCall {
33313359
/// Allocate a new IseqCall
3332-
fn new(iseq: IseqPtr, jit_entry_idx: u32) -> IseqCallRef {
3360+
fn new(iseq: IseqPtr, jit_entry_idx: u16, argc: u16) -> IseqCallRef {
33333361
let iseq_call = IseqCall {
33343362
iseq: Cell::new(iseq),
33353363
start_addr: Cell::new(None),
33363364
end_addr: Cell::new(None),
33373365
jit_entry_idx,
3366+
argc,
33383367
};
33393368
Rc::new(iseq_call)
33403369
}

zjit/src/hir/opt_tests.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14857,6 +14857,53 @@ 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+
14883+
#[test]
14884+
fn test_exit_from_function_stub_for_lead_opt() {
14885+
// We have a SendDirect to a callee that fails to compile,
14886+
// so the function stub has to take care of exiting to
14887+
// interpreter.
14888+
let result = eval("
14889+
def target(_required, a = a, b = b)
14890+
::RubyVM::ZJIT.induce_compile_failure!
14891+
a
14892+
end
14893+
14894+
def entry = target(1)
14895+
14896+
entry
14897+
entry
14898+
");
14899+
assert_eq!(Qnil, result);
14900+
14901+
crate::hir::tests::hir_build_tests::assert_compile_fails("target", ParseError::DirectiveInduced);
14902+
let hir = hir_string("entry");
14903+
assert!(hir.contains("SendDirect"), "{hir}");
14904+
}
14905+
14906+
1486014907
#[test]
1486114908
fn test_recompile_no_profile_send() {
1486214909
// 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)