Skip to content

Commit 11f1213

Browse files
authored
YJIT: Support splat with C methods with -1 arity
Usually we deal with splats by speculating that they're of a specific size. In this case, the C method takes a pointer and a length, so we can support changing sizes just fine.
1 parent 1f740cd commit 11f1213

File tree

6 files changed

+133
-15
lines changed

6 files changed

+133
-15
lines changed

bootstraptest/test_yjit.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4634,3 +4634,27 @@ def splat_nil_kw_splat(args) = identity(*args, **nil)
46344634
empty = []
46354635
[1.abs(*empty), 1.abs(**nil), 1.bit_length(*empty, **nil)]
46364636
}
4637+
4638+
# splat into C methods with -1 arity
4639+
assert_equal '[[1, 2, 3], [0, 2, 3], [1, 2, 3], [2, 2, 3], [], [], [{}]]', %q{
4640+
class Foo < Array
4641+
def push(args) = super(1, *args)
4642+
end
4643+
4644+
def test_cfunc_vargs_splat(sub_instance, array_class, empty_kw_hash)
4645+
splat = [2, 3]
4646+
kw_splat = [empty_kw_hash]
4647+
[
4648+
sub_instance.push(splat),
4649+
array_class[0, *splat, **nil],
4650+
array_class[1, *splat, &nil],
4651+
array_class[2, *splat, **nil, &nil],
4652+
array_class.send(:[], *kw_splat),
4653+
# kw_splat disables keywords hash handling
4654+
array_class[*kw_splat],
4655+
array_class[*kw_splat, **nil],
4656+
]
4657+
end
4658+
4659+
test_cfunc_vargs_splat(Foo.new, Array, Hash.ruby2_keywords_hash({}))
4660+
}

yjit.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,51 @@ rb_yjit_ruby2_keywords_splat_p(VALUE obj)
890890
return FL_TEST_RAW(last, RHASH_PASS_AS_KEYWORDS);
891891
}
892892

893+
// Checks to establish preconditions for rb_yjit_splat_varg_cfunc()
894+
VALUE
895+
rb_yjit_splat_varg_checks(VALUE *sp, VALUE splat_array, rb_control_frame_t *cfp)
896+
{
897+
// We inserted a T_ARRAY guard before this call
898+
long len = RARRAY_LEN(splat_array);
899+
900+
// Large splat arrays need a separate allocation
901+
if (len < 0 || len > VM_ARGC_STACK_MAX) return Qfalse;
902+
903+
// Would we overflow if we put the contents of the array onto the stack?
904+
if (sp + len > (VALUE *)(cfp - 2)) return Qfalse;
905+
906+
return Qtrue;
907+
}
908+
909+
// Push array elements to the stack for a C method that has a variable number
910+
// of parameters. Returns the number of arguments the splat array contributes.
911+
int
912+
rb_yjit_splat_varg_cfunc(VALUE *stack_splat_array, bool sole_splat)
913+
{
914+
VALUE splat_array = *stack_splat_array;
915+
int len;
916+
917+
// We already checked that length fits in `int`
918+
RUBY_ASSERT(RB_TYPE_P(splat_array, T_ARRAY));
919+
len = (int)RARRAY_LEN(splat_array);
920+
921+
// If this is a splat call without any keyword arguments, exclude the
922+
// ruby2_keywords hash if it's empty
923+
if (sole_splat && len > 0) {
924+
VALUE last_hash = RARRAY_AREF(splat_array, len - 1);
925+
if (RB_TYPE_P(last_hash, T_HASH) &&
926+
FL_TEST_RAW(last_hash, RHASH_PASS_AS_KEYWORDS) &&
927+
RHASH_EMPTY_P(last_hash)) {
928+
len--;
929+
}
930+
}
931+
932+
// Push the contents of the array onto the stack
933+
MEMCPY(stack_splat_array, RARRAY_CONST_PTR(splat_array), VALUE, len);
934+
935+
return len;
936+
}
937+
893938
// Print the Ruby source location of some ISEQ for debugging purposes
894939
void
895940
rb_yjit_dump_iseq_loc(const rb_iseq_t *iseq, uint32_t insn_idx)

yjit/bindgen/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@ fn main() {
460460
.allowlist_function("rb_vm_base_ptr")
461461
.allowlist_function("rb_ec_stack_check")
462462
.allowlist_function("rb_vm_top_self")
463+
.allowlist_function("rb_yjit_splat_varg_checks")
464+
.allowlist_function("rb_yjit_splat_varg_cfunc")
463465

464466
// We define VALUE manually, don't import it
465467
.blocklist_type("VALUE")

yjit/src/codegen.rs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6140,18 +6140,16 @@ fn gen_send_cfunc(
61406140
let cfunc_argc = unsafe { get_mct_argc(cfunc) };
61416141
let mut argc = argc;
61426142

6143+
// Splat call to a C method that takes `VALUE *` and `len`
6144+
let variable_splat = flags & VM_CALL_ARGS_SPLAT != 0 && cfunc_argc == -1;
6145+
let block_arg = flags & VM_CALL_ARGS_BLOCKARG != 0;
6146+
61436147
// If the function expects a Ruby array of arguments
61446148
if cfunc_argc < 0 && cfunc_argc != -1 {
61456149
gen_counter_incr(asm, Counter::send_cfunc_ruby_array_varg);
61466150
return None;
61476151
}
61486152

6149-
// We aren't handling a vararg cfuncs with splat currently.
6150-
if flags & VM_CALL_ARGS_SPLAT != 0 && cfunc_argc == -1 {
6151-
gen_counter_incr(asm, Counter::send_args_splat_cfunc_var_args);
6152-
return None;
6153-
}
6154-
61556153
exit_if_kwsplat_non_nil(asm, flags, Counter::send_cfunc_kw_splat_non_nil)?;
61566154
let kw_splat = flags & VM_CALL_KW_SPLAT != 0;
61576155

@@ -6217,6 +6215,18 @@ fn gen_send_cfunc(
62176215
asm.cmp(CFP, stack_limit);
62186216
asm.jbe(Target::side_exit(Counter::guard_send_se_cf_overflow));
62196217

6218+
// Guard for variable length splat call before any modifications to the stack
6219+
if variable_splat {
6220+
let splat_array = asm.stack_opnd(i32::from(kw_splat) + i32::from(block_arg));
6221+
guard_object_is_array(asm, splat_array, splat_array.into(), Counter::guard_send_splat_not_array);
6222+
6223+
asm_comment!(asm, "guard variable length splat call servicable");
6224+
let sp = asm.ctx.sp_opnd(0);
6225+
let proceed = asm.ccall(rb_yjit_splat_varg_checks as _, vec![sp, splat_array, CFP]);
6226+
asm.cmp(proceed, Qfalse.into());
6227+
asm.je(Target::side_exit(Counter::guard_send_cfunc_bad_splat_vargs));
6228+
}
6229+
62206230
// Number of args which will be passed through to the callee
62216231
// This is adjusted by the kwargs being combined into a hash.
62226232
let mut passed_argc = if kw_arg.is_null() {
@@ -6242,7 +6252,6 @@ fn gen_send_cfunc(
62426252
return None;
62436253
}
62446254

6245-
let block_arg = flags & VM_CALL_ARGS_BLOCKARG != 0;
62466255
let block_arg_type = if block_arg {
62476256
Some(asm.ctx.get_opnd_type(StackOpnd(0)))
62486257
} else {
@@ -6287,9 +6296,9 @@ fn gen_send_cfunc(
62876296
argc -= 1;
62886297
}
62896298

6290-
// push_splat_args does stack manipulation so we can no longer side exit
6291-
if flags & VM_CALL_ARGS_SPLAT != 0 {
6292-
assert!(cfunc_argc >= 0);
6299+
// Splat handling when C method takes a static number of arguments.
6300+
// push_splat_args() does stack manipulation so we can no longer side exit
6301+
if flags & VM_CALL_ARGS_SPLAT != 0 && cfunc_argc >= 0 {
62936302
let required_args : u32 = (cfunc_argc as u32).saturating_sub(argc as u32 - 1);
62946303
// + 1 because we pass self
62956304
if required_args + 1 >= C_ARG_OPNDS.len() as u32 {
@@ -6312,15 +6321,34 @@ fn gen_send_cfunc(
63126321
handle_opt_send_shift_stack(asm, argc);
63136322
}
63146323

6324+
// Push a dynamic number of items from the splat array to the stack when calling a vargs method
6325+
let dynamic_splat_size = if variable_splat {
6326+
asm_comment!(asm, "variable length splat");
6327+
let just_splat = usize::from(!kw_splat && kw_arg.is_null()).into();
6328+
let stack_splat_array = asm.lea(asm.stack_opnd(0));
6329+
Some(asm.ccall(rb_yjit_splat_varg_cfunc as _, vec![stack_splat_array, just_splat]))
6330+
} else {
6331+
None
6332+
};
6333+
63156334
// Points to the receiver operand on the stack
63166335
let recv = asm.stack_opnd(argc);
63176336

63186337
// Store incremented PC into current control frame in case callee raises.
63196338
jit_save_pc(jit, asm);
63206339

6321-
// Increment the stack pointer by 3 (in the callee)
6322-
// sp += 3
6323-
let sp = asm.lea(asm.ctx.sp_opnd(SIZEOF_VALUE_I32 * 3));
6340+
// Find callee's SP with space for metadata.
6341+
// Usually sp+3.
6342+
let sp = if let Some(splat_size) = dynamic_splat_size {
6343+
// Compute the callee's SP at runtime in case we accept a variable size for the splat array
6344+
const _: () = assert!(SIZEOF_VALUE == 8, "opting for a shift since mul on A64 takes no immediates");
6345+
let splat_size_bytes = asm.lshift(splat_size, 3usize.into());
6346+
// 3 items for method metadata, minus one to remove the splat array
6347+
let static_stack_top = asm.lea(asm.ctx.sp_opnd(SIZEOF_VALUE_I32 * 2));
6348+
asm.add(static_stack_top, splat_size_bytes)
6349+
} else {
6350+
asm.lea(asm.ctx.sp_opnd(SIZEOF_VALUE_I32 * 3))
6351+
};
63246352

63256353
let specval = if block_arg_type == Some(Type::BlockParamProxy) {
63266354
SpecVal::BlockHandler(Some(BlockHandler::BlockParamProxy))
@@ -6382,8 +6410,17 @@ fn gen_send_cfunc(
63826410
else if cfunc_argc == -1 {
63836411
// The method gets a pointer to the first argument
63846412
// rb_f_puts(int argc, VALUE *argv, VALUE recv)
6413+
6414+
let passed_argc_opnd = if let Some(splat_size) = dynamic_splat_size {
6415+
// The final argc is the size of the splat, minus one for the splat array itself
6416+
asm.add(splat_size, (passed_argc - 1).into())
6417+
} else {
6418+
// Without a splat, passed_argc is static
6419+
Opnd::Imm(passed_argc.into())
6420+
};
6421+
63856422
vec![
6386-
Opnd::Imm(passed_argc.into()),
6423+
passed_argc_opnd,
63876424
asm.lea(asm.ctx.sp_opnd(-argc * SIZEOF_VALUE_I32)),
63886425
asm.stack_opnd(argc),
63896426
]

yjit/src/cruby_bindings.inc.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,15 @@ extern "C" {
11941194
pub fn rb_yjit_fix_div_fix(recv: VALUE, obj: VALUE) -> VALUE;
11951195
pub fn rb_yjit_fix_mod_fix(recv: VALUE, obj: VALUE) -> VALUE;
11961196
pub fn rb_yjit_ruby2_keywords_splat_p(obj: VALUE) -> usize;
1197+
pub fn rb_yjit_splat_varg_checks(
1198+
sp: *mut VALUE,
1199+
splat_array: VALUE,
1200+
cfp: *mut rb_control_frame_t,
1201+
) -> VALUE;
1202+
pub fn rb_yjit_splat_varg_cfunc(
1203+
stack_splat_array: *mut VALUE,
1204+
sole_splat: bool,
1205+
) -> ::std::os::raw::c_int;
11971206
pub fn rb_yjit_dump_iseq_loc(iseq: *const rb_iseq_t, insn_idx: u32);
11981207
pub fn rb_yjit_iseq_inspect(iseq: *const rb_iseq_t) -> *mut ::std::os::raw::c_char;
11991208
pub fn rb_FL_TEST(obj: VALUE, flags: VALUE) -> VALUE;

yjit/src/stats.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,6 @@ make_counters! {
386386
send_args_splat_aref,
387387
send_args_splat_aset,
388388
send_args_splat_opt_call,
389-
send_args_splat_cfunc_var_args,
390389
send_iseq_splat_arity_error,
391390
send_splat_too_long,
392391
send_send_wrong_args,
@@ -444,6 +443,8 @@ make_counters! {
444443
guard_send_not_string,
445444
guard_send_respond_to_mid_mismatch,
446445

446+
guard_send_cfunc_bad_splat_vargs,
447+
447448
guard_invokesuper_me_changed,
448449

449450
guard_invokeblock_tag_changed,

0 commit comments

Comments
 (0)