Skip to content

Commit 01096a6

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 01096a6

5 files changed

Lines changed: 101 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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,8 @@ pub enum SendFallbackReason {
667667
CCallWithFrameTooManyArgs,
668668
ObjToStringNotString,
669669
TooManyArgsForLir,
670+
/// Too many arguments to fit in a u16 for IseqCall
671+
TooManyArgs,
670672
/// The Proc object for a BMETHOD is not defined by an ISEQ. (See `enum rb_block_type`.)
671673
BmethodNonIseqProc,
672674
/// Caller supplies too few or too many arguments than what the callee's parameters expects.
@@ -731,6 +733,7 @@ impl Display for SendFallbackReason {
731733
CCallWithFrameTooManyArgs => write!(f, "CCallWithFrame: too many arguments"),
732734
ObjToStringNotString => write!(f, "ObjToString: result is not a string"),
733735
TooManyArgsForLir => write!(f, "Too many arguments for LIR"),
736+
TooManyArgs => write!(f, "Too many arguments"),
734737
BmethodNonIseqProc => write!(f, "Bmethod: Proc object is not defined by an ISEQ"),
735738
ArgcParamMismatch => write!(f, "Argument count does not match parameter count"),
736739
ComplexArgPass => write!(f, "Complex argument passing"),
@@ -2356,6 +2359,13 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq
23562359
return false;
23572360
}
23582361

2362+
// IseqCall stores num_optionals_passed and argc as u16.
2363+
// num_optionals_passed <= caller_positional <= args.len(), so checking args suffices.
2364+
if u16::try_from(args.len()).is_err() {
2365+
function.set_dynamic_send_reason(send_insn, TooManyArgs);
2366+
return false;
2367+
}
2368+
23592369
can_send
23602370
}
23612371

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

zjit/src/stats.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ make_counters! {
249249
send_fallback_send_without_block_not_optimized_method_type_optimized,
250250
send_fallback_send_without_block_not_optimized_need_permission,
251251
send_fallback_too_many_args_for_lir,
252+
send_fallback_too_many_args,
252253
send_fallback_send_without_block_bop_redefined,
253254
send_fallback_send_without_block_operands_not_fixnum,
254255
send_fallback_send_without_block_polymorphic_fallback,
@@ -651,6 +652,7 @@ pub fn send_fallback_counter(reason: crate::hir::SendFallbackReason) -> Counter
651652
SendWithoutBlockNotOptimizedNeedPermission
652653
=> send_fallback_send_without_block_not_optimized_need_permission,
653654
TooManyArgsForLir => send_fallback_too_many_args_for_lir,
655+
TooManyArgs => send_fallback_too_many_args,
654656
SendWithoutBlockBopRedefined => send_fallback_send_without_block_bop_redefined,
655657
SendWithoutBlockOperandsNotFixnum => send_fallback_send_without_block_operands_not_fixnum,
656658
SendWithoutBlockPolymorphicFallback => send_fallback_send_without_block_polymorphic_fallback,

0 commit comments

Comments
 (0)