Skip to content

Commit 0c67501

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 768a6cf commit 0c67501

4 files changed

Lines changed: 95 additions & 13 deletions

File tree

zjit/src/codegen.rs

Lines changed: 40 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,44 @@ 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+
// When there are unfilled optionals
2992+
let opt_num: usize = params.opt_num.try_into().expect("ISEQ opt_num should be non-negative");
2993+
let opts_unfilled = opt_num.saturating_sub(num_opts_filled.to_usize());
2994+
if opts_unfilled > 0 {
2995+
let lead_num: usize = params.lead_num.try_into().expect("ISEQ lead_num should be non-negative");
2996+
let param_locals = &mut locals[..params_size];
2997+
let args_after_opts = lead_num + num_opts_filled.to_usize()..argc.to_usize();
2998+
let after_opts_param_idx = lead_num + opt_num;
2999+
// Move to the right args that are after the unfilled optionals
3000+
if let Some(moved_by) = after_opts_param_idx.checked_sub(args_after_opts.start) {
3001+
if after_opts_param_idx + args_after_opts.len() <= param_locals.len() {
3002+
param_locals.copy_within(args_after_opts.clone(), after_opts_param_idx);
3003+
// Nil-fill unfilled optionals
3004+
param_locals.get_mut(args_after_opts.start..args_after_opts.start + moved_by).unwrap_or_default().fill(Qnil);
3005+
}
3006+
}
3007+
}
29833008
}
29843009

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

3006-
prepare_for_exit(iseq, cfp, sp, compile_error);
3031+
prepare_for_exit(iseq, cfp, sp, argc, num_opts_filled, compile_error);
30073032
return ZJITState::get_exit_trampoline_with_counter().raw_ptr(cb);
30083033
}
30093034

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

3021-
prepare_for_exit(iseq, cfp, sp, &compile_error);
3046+
prepare_for_exit(iseq, cfp, sp, argc, num_opts_filled, &compile_error);
30223047
ZJITState::get_exit_trampoline_with_counter()
30233048
});
30243049
cb.mark_all_executable();
@@ -3343,7 +3368,10 @@ pub struct IseqCall {
33433368
pub iseq: Cell<IseqPtr>,
33443369

33453370
/// Index that corresponds to [crate::hir::jit_entry_insns]
3346-
jit_entry_idx: u32,
3371+
jit_entry_idx: u16,
3372+
3373+
/// Argument count passing to the HIR function
3374+
argc: u16,
33473375

33483376
/// Position where the call instruction starts
33493377
start_addr: Cell<Option<CodePtr>>,
@@ -3356,12 +3384,13 @@ pub type IseqCallRef = Rc<IseqCall>;
33563384

33573385
impl IseqCall {
33583386
/// Allocate a new IseqCall
3359-
fn new(iseq: IseqPtr, jit_entry_idx: u32) -> IseqCallRef {
3387+
fn new(iseq: IseqPtr, jit_entry_idx: u16, argc: u16) -> IseqCallRef {
33603388
let iseq_call = IseqCall {
33613389
iseq: Cell::new(iseq),
33623390
start_addr: Cell::new(None),
33633391
end_addr: Cell::new(None),
33643392
jit_entry_idx,
3393+
argc,
33653394
};
33663395
Rc::new(iseq_call)
33673396
}

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: 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)