Skip to content

Commit ac1e9e4

Browse files
committed
YJIT: Fix ruby2_keywords splat+rest and drop bogus checks
YJIT didn't guard for ruby2_keywords hash in case of splat calls that land in methods with a rest parameter, creating incorrect results. The compile-time checks didn't correspond to any actual effects of ruby2_keywords, so it was masking this bug and YJIT was needlessly refusing to compile some code. About 16% of fallback reasons in `lobsters` was due to the ISeq check. We already handle the tagging part with exit_if_supplying_kw_and_has_no_kw() and should now have a dynamic guard for all splat cases. Note for backporting: You also need 7f51959. [Bug #20195]
1 parent c0cabc0 commit ac1e9e4

File tree

7 files changed

+41
-31
lines changed

7 files changed

+41
-31
lines changed

bootstraptest/test_yjit.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4443,3 +4443,19 @@ def entry
44434443
def defined_yield = defined?(yield)
44444444
[defined_yield, defined_yield {}]
44454445
}
4446+
4447+
# splat with ruby2_keywords into rest parameter
4448+
assert_equal '[[{:a=>1}], {}]', %q{
4449+
ruby2_keywords def foo(*args) = args
4450+
4451+
def bar(*args, **kw) = [args, kw]
4452+
4453+
def pass_bar(*args) = bar(*args)
4454+
4455+
def body
4456+
args = foo(a: 1)
4457+
pass_bar(*args)
4458+
end
4459+
4460+
body
4461+
}

yjit.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,19 @@ rb_yjit_fix_mod_fix(VALUE recv, VALUE obj)
871871
return rb_fix_mod_fix(recv, obj);
872872
}
873873

874+
// Return non-zero when `obj` is an array and its last item is a
875+
// `ruby2_keywords` hash. We don't support this kind of splat.
876+
size_t
877+
rb_yjit_ruby2_keywords_splat_p(VALUE obj)
878+
{
879+
if (!RB_TYPE_P(obj, T_ARRAY)) return 0;
880+
long len = RARRAY_LEN(obj);
881+
if (len == 0) return 0;
882+
VALUE last = RARRAY_AREF(obj, len - 1);
883+
if (!RB_TYPE_P(last, T_HASH)) return 0;
884+
return FL_TEST_RAW(last, RHASH_PASS_AS_KEYWORDS);
885+
}
886+
874887
// Print the Ruby source location of some ISEQ for debugging purposes
875888
void
876889
rb_yjit_dump_iseq_loc(const rb_iseq_t *iseq, uint32_t insn_idx)

yjit/bindgen/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ fn main() {
426426
.allowlist_function("rb_yarv_str_eql_internal")
427427
.allowlist_function("rb_str_neq_internal")
428428
.allowlist_function("rb_yarv_ary_entry_internal")
429+
.allowlist_function("rb_yjit_ruby2_keywords_splat_p")
429430
.allowlist_function("rb_yjit_fix_div_fix")
430431
.allowlist_function("rb_yjit_fix_mod_fix")
431432
.allowlist_function("rb_FL_TEST")

yjit/src/codegen.rs

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5555,18 +5555,6 @@ fn gen_send_cfunc(
55555555
return None;
55565556
}
55575557

5558-
// In order to handle backwards compatibility between ruby 3 and 2
5559-
// ruby2_keywords was introduced. It is called only on methods
5560-
// with splat and changes they way they handle them.
5561-
// We are just going to not compile these.
5562-
// https://docs.ruby-lang.org/en/3.2/Module.html#method-i-ruby2_keywords
5563-
if unsafe {
5564-
get_iseq_flags_ruby2_keywords(jit.iseq) && flags & VM_CALL_ARGS_SPLAT != 0
5565-
} {
5566-
gen_counter_incr(asm, Counter::send_args_splat_cfunc_ruby2_keywords);
5567-
return None;
5568-
}
5569-
55705558
let kw_arg = unsafe { vm_ci_kwarg(ci) };
55715559
let kw_arg_num = if kw_arg.is_null() {
55725560
0
@@ -6147,7 +6135,6 @@ fn gen_send_iseq(
61476135
exit_if_has_post(asm, iseq)?;
61486136
exit_if_has_kwrest(asm, iseq)?;
61496137
exit_if_kw_splat(asm, flags)?;
6150-
exit_if_splat_and_ruby2_keywords(asm, jit, flags)?;
61516138
exit_if_has_rest_and_captured(asm, iseq_has_rest, captured_opnd)?;
61526139
exit_if_has_rest_and_supplying_kws(asm, iseq_has_rest, iseq, supplying_kws)?;
61536140
exit_if_supplying_kw_and_has_no_kw(asm, supplying_kws, iseq)?;
@@ -6426,6 +6413,8 @@ fn gen_send_iseq(
64266413
asm.jbe(Target::side_exit(Counter::guard_send_se_cf_overflow));
64276414

64286415
if iseq_has_rest && flags & VM_CALL_ARGS_SPLAT != 0 {
6416+
let splat_pos = i32::from(block_arg) + kw_arg_num;
6417+
64296418
// Insert length guard for a call to copy_splat_args_for_rest_callee()
64306419
// that will come later. We will have made changes to
64316420
// the stack by spilling or handling __send__ shifting
@@ -6439,12 +6428,19 @@ fn gen_send_iseq(
64396428
if take_count > 0 {
64406429
asm_comment!(asm, "guard splat_array_length >= {take_count}");
64416430

6442-
let splat_array = asm.stack_opnd(i32::from(block_arg) + kw_arg_num);
6431+
let splat_array = asm.stack_opnd(splat_pos);
64436432
let array_len_opnd = get_array_len(asm, splat_array);
64446433
asm.cmp(array_len_opnd, take_count.into());
64456434
asm.jl(Target::side_exit(Counter::guard_send_iseq_has_rest_and_splat_too_few));
64466435
}
64476436
}
6437+
6438+
// All splats need to guard for ruby2_keywords hash. Check with a function call when
6439+
// splatting into a rest param since the index for the last item in the array is dynamic.
6440+
asm_comment!(asm, "guard no ruby2_keywords hash in splat");
6441+
let bad_splat = asm.ccall(rb_yjit_ruby2_keywords_splat_p as _, vec![asm.stack_opnd(splat_pos)]);
6442+
asm.cmp(bad_splat, 0.into());
6443+
asm.jnz(Target::side_exit(Counter::guard_send_splatarray_last_ruby_2_keywords));
64486444
}
64496445

64506446
match block_arg_type {
@@ -7008,20 +7004,6 @@ fn exit_if_kw_splat(asm: &mut Assembler, flags: u32) -> Option<()> {
70087004
exit_if(asm, flags & VM_CALL_KW_SPLAT != 0, Counter::send_iseq_kw_splat)
70097005
}
70107006

7011-
#[must_use]
7012-
fn exit_if_splat_and_ruby2_keywords(asm: &mut Assembler, jit: &mut JITState, flags: u32) -> Option<()> {
7013-
// In order to handle backwards compatibility between ruby 3 and 2
7014-
// ruby2_keywords was introduced. It is called only on methods
7015-
// with splat and changes they way they handle them.
7016-
// We are just going to not compile these.
7017-
// https://www.rubydoc.info/stdlib/core/Proc:ruby2_keywords
7018-
exit_if(
7019-
asm,
7020-
unsafe { get_iseq_flags_ruby2_keywords(jit.iseq) } && flags & VM_CALL_ARGS_SPLAT != 0,
7021-
Counter::send_iseq_ruby2_keywords,
7022-
)
7023-
}
7024-
70257007
#[must_use]
70267008
fn exit_if_has_rest_and_captured(asm: &mut Assembler, iseq_has_rest: bool, captured_opnd: Option<Opnd>) -> Option<()> {
70277009
exit_if(asm, iseq_has_rest && captured_opnd.is_some(), Counter::send_iseq_has_rest_and_captured)

yjit/src/cruby.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,6 @@ pub use rb_get_iseq_flags_has_lead as get_iseq_flags_has_lead;
165165
pub use rb_get_iseq_flags_has_opt as get_iseq_flags_has_opt;
166166
pub use rb_get_iseq_flags_has_kw as get_iseq_flags_has_kw;
167167
pub use rb_get_iseq_flags_has_rest as get_iseq_flags_has_rest;
168-
pub use rb_get_iseq_flags_ruby2_keywords as get_iseq_flags_ruby2_keywords;
169168
pub use rb_get_iseq_flags_has_post as get_iseq_flags_has_post;
170169
pub use rb_get_iseq_flags_has_kwrest as get_iseq_flags_has_kwrest;
171170
pub use rb_get_iseq_flags_has_block as get_iseq_flags_has_block;

yjit/src/cruby_bindings.inc.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,7 @@ extern "C" {
11401140
pub fn rb_yjit_rb_ary_subseq_length(ary: VALUE, beg: ::std::os::raw::c_long) -> VALUE;
11411141
pub fn rb_yjit_fix_div_fix(recv: VALUE, obj: VALUE) -> VALUE;
11421142
pub fn rb_yjit_fix_mod_fix(recv: VALUE, obj: VALUE) -> VALUE;
1143+
pub fn rb_yjit_ruby2_keywords_splat_p(obj: VALUE) -> usize;
11431144
pub fn rb_yjit_dump_iseq_loc(iseq: *const rb_iseq_t, insn_idx: u32);
11441145
pub fn rb_FL_TEST(obj: VALUE, flags: VALUE) -> VALUE;
11451146
pub fn rb_FL_TEST_RAW(obj: VALUE, flags: VALUE) -> VALUE;

yjit/src/stats.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,10 +378,8 @@ make_counters! {
378378
send_args_splat_opt_call,
379379
send_args_splat_cfunc_var_args,
380380
send_args_splat_cfunc_zuper,
381-
send_args_splat_cfunc_ruby2_keywords,
382381
send_iseq_splat_arity_error,
383382
send_splat_too_long,
384-
send_iseq_ruby2_keywords,
385383
send_send_not_imm,
386384
send_send_wrong_args,
387385
send_send_null_mid,

0 commit comments

Comments
 (0)