Skip to content

Commit 3174c43

Browse files
committed
ZJIT: Handle caller_kwarg in direct send when all keyword params are required
1 parent 6e27995 commit 3174c43

3 files changed

Lines changed: 183 additions & 13 deletions

File tree

zjit/src/hir.rs

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,10 @@ pub enum SendFallbackReason {
595595
SendWithoutBlockNotOptimizedMethodTypeOptimized(OptimizedMethodType),
596596
SendWithoutBlockBopRedefined,
597597
SendWithoutBlockOperandsNotFixnum,
598+
SendWithoutBlockDirectKeywordMismatch,
599+
SendWithoutBlockDirectOptionalKeywords,
600+
SendWithoutBlockDirectKeywordCountMismatch,
601+
SendWithoutBlockDirectMissingKeyword,
598602
SendPolymorphic,
599603
SendMegamorphic,
600604
SendNoProfiles,
@@ -611,6 +615,8 @@ pub enum SendFallbackReason {
611615
/// The call has at least one feature on the caller or callee side that the optimizer does not
612616
/// support.
613617
ComplexArgPass,
618+
/// Caller has keyword arguments but callee doesn't expect them; need to convert to hash.
619+
UnexpectedKeywordArgs,
614620
/// Initial fallback reason for every instruction, which should be mutated to
615621
/// a more actionable reason when an attempt to specialize the instruction fails.
616622
Uncategorized(ruby_vminsn_type),
@@ -1475,7 +1481,20 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq
14751481
use Counter::*;
14761482
if unsafe { rb_get_iseq_flags_has_rest(iseq) } { count_failure(complex_arg_pass_param_rest) }
14771483
if unsafe { rb_get_iseq_flags_has_post(iseq) } { count_failure(complex_arg_pass_param_post) }
1478-
if unsafe { rb_get_iseq_flags_has_kw(iseq) } { count_failure(complex_arg_pass_param_kw) }
1484+
1485+
// We support required-only keywords, but not optional keywords yet
1486+
if unsafe { rb_get_iseq_flags_has_kw(iseq) } {
1487+
let keyword = unsafe { get_iseq_body_param_keyword(iseq) };
1488+
if !keyword.is_null() {
1489+
let num = unsafe { (*keyword).num };
1490+
let required_num = unsafe { (*keyword).required_num };
1491+
// Only support required keywords for now (no optional keywords)
1492+
if num != required_num {
1493+
count_failure(complex_arg_pass_param_kw_opt)
1494+
}
1495+
}
1496+
}
1497+
14791498
if unsafe { rb_get_iseq_flags_has_kwrest(iseq) } { count_failure(complex_arg_pass_param_kwrest) }
14801499
if unsafe { rb_get_iseq_flags_has_block(iseq) } { count_failure(complex_arg_pass_param_block) }
14811500
if unsafe { rb_get_iseq_flags_forwardable(iseq) } { count_failure(complex_arg_pass_param_forwardable) }
@@ -1489,9 +1508,12 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq
14891508
// relies on rejecting features above.
14901509
let lead_num = unsafe { get_iseq_body_param_lead_num(iseq) };
14911510
let opt_num = unsafe { get_iseq_body_param_opt_num(iseq) };
1511+
let keyword = unsafe { get_iseq_body_param_keyword(iseq) };
1512+
let kw_req_num = if keyword.is_null() { 0 } else { unsafe { (*keyword).required_num } };
1513+
let req_num = lead_num + kw_req_num;
14921514
can_send = c_int::try_from(args.len())
14931515
.as_ref()
1494-
.map(|argc| (lead_num..=lead_num + opt_num).contains(argc))
1516+
.map(|argc| (req_num..=req_num + opt_num).contains(argc))
14951517
.unwrap_or(false);
14961518
if !can_send {
14971519
function.set_dynamic_send_reason(send_insn, ArgcParamMismatch);
@@ -2177,6 +2199,72 @@ impl Function {
21772199
}
21782200
}
21792201

2202+
/// Reorder keyword arguments to match the callee's expectation.
2203+
///
2204+
/// Returns Ok with reordered arguments if successful, or Err with the fallback reason if not.
2205+
fn reorder_keyword_arguments(
2206+
&self,
2207+
args: &[InsnId],
2208+
kwarg: *const rb_callinfo_kwarg,
2209+
iseq: IseqPtr,
2210+
) -> Result<Vec<InsnId>, SendFallbackReason> {
2211+
let callee_keyword = unsafe { get_iseq_body_param_keyword(iseq) };
2212+
if callee_keyword.is_null() {
2213+
// Caller is passing kwargs but callee doesn't expect them.
2214+
return Err(SendWithoutBlockDirectKeywordMismatch);
2215+
}
2216+
2217+
let caller_kw_count = unsafe { get_cikw_keyword_len(kwarg) } as usize;
2218+
let callee_kw_count = unsafe { (*callee_keyword).num } as usize;
2219+
let callee_kw_required = unsafe { (*callee_keyword).required_num } as usize;
2220+
let callee_kw_table = unsafe { (*callee_keyword).table };
2221+
2222+
// For now, only handle the case where all keywords are required.
2223+
if callee_kw_count != callee_kw_required {
2224+
return Err(SendWithoutBlockDirectOptionalKeywords);
2225+
}
2226+
if caller_kw_count != callee_kw_count {
2227+
return Err(SendWithoutBlockDirectKeywordCountMismatch);
2228+
}
2229+
2230+
// The keyword arguments are the last arguments in the args vector.
2231+
let kw_args_start = args.len() - caller_kw_count;
2232+
2233+
// Build a mapping from caller keywords to their positions.
2234+
let mut caller_kw_order: Vec<ID> = Vec::with_capacity(caller_kw_count);
2235+
for i in 0..caller_kw_count {
2236+
let sym = unsafe { get_cikw_keywords_idx(kwarg, i as i32) };
2237+
let id = unsafe { rb_sym2id(sym) };
2238+
caller_kw_order.push(id);
2239+
}
2240+
2241+
// Reorder keyword arguments to match callee expectation.
2242+
let mut reordered_kw_args: Vec<InsnId> = Vec::with_capacity(callee_kw_count);
2243+
for i in 0..callee_kw_count {
2244+
let expected_id = unsafe { *callee_kw_table.add(i) };
2245+
2246+
// Find where this keyword is in the caller's order
2247+
let mut found = false;
2248+
for (j, &caller_id) in caller_kw_order.iter().enumerate() {
2249+
if caller_id == expected_id {
2250+
reordered_kw_args.push(args[kw_args_start + j]);
2251+
found = true;
2252+
break;
2253+
}
2254+
}
2255+
2256+
if !found {
2257+
// Required keyword not provided by caller which will raise an ArgumentError.
2258+
return Err(SendWithoutBlockDirectMissingKeyword);
2259+
}
2260+
}
2261+
2262+
// Replace the keyword arguments with the reordered ones.
2263+
let mut processed_args = args[..kw_args_start].to_vec();
2264+
processed_args.extend(reordered_kw_args);
2265+
Ok(processed_args)
2266+
}
2267+
21802268
/// Resolve the receiver type for method dispatch optimization.
21812269
///
21822270
/// Takes the receiver's Type, receiver HIR instruction, and ISEQ instruction index.
@@ -2396,7 +2484,29 @@ impl Function {
23962484
if let Some(profiled_type) = profiled_type {
23972485
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
23982486
}
2399-
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args, state });
2487+
2488+
// Check if caller is passing keywords but callee doesn't expect them.
2489+
let kwarg = unsafe { rb_vm_ci_kwarg(ci) };
2490+
if !kwarg.is_null() && !unsafe { rb_get_iseq_flags_has_kw(iseq) } {
2491+
// Caller has keywords but callee doesn't; Need to convert to hash.
2492+
self.set_dynamic_send_reason(insn_id, UnexpectedKeywordArgs);
2493+
self.push_insn_id(block, insn_id); continue;
2494+
}
2495+
2496+
// Handle keyword argument reordering if present.
2497+
let processed_args = if !kwarg.is_null() {
2498+
match self.reorder_keyword_arguments(&args, kwarg, iseq) {
2499+
Ok(reordered) => reordered,
2500+
Err(reason) => {
2501+
self.set_dynamic_send_reason(insn_id, reason);
2502+
self.push_insn_id(block, insn_id); continue;
2503+
}
2504+
}
2505+
} else {
2506+
args.clone()
2507+
};
2508+
2509+
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, state });
24002510
self.make_equal_to(insn_id, send_direct);
24012511
} else if def_type == VM_METHOD_TYPE_BMETHOD {
24022512
let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) };
@@ -2431,7 +2541,29 @@ impl Function {
24312541
if let Some(profiled_type) = profiled_type {
24322542
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
24332543
}
2434-
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args, state });
2544+
2545+
// Check if caller is passing keywords but callee doesn't expect them.
2546+
let kwarg = unsafe { rb_vm_ci_kwarg(ci) };
2547+
if !kwarg.is_null() && !unsafe { rb_get_iseq_flags_has_kw(iseq) } {
2548+
// Caller has keywords but callee doesn't; Need to convert to hash.
2549+
self.set_dynamic_send_reason(insn_id, UnexpectedKeywordArgs);
2550+
self.push_insn_id(block, insn_id); continue;
2551+
}
2552+
2553+
// Handle keyword argument reordering if present.
2554+
let processed_args = if !kwarg.is_null() {
2555+
match self.reorder_keyword_arguments(&args, kwarg, iseq) {
2556+
Ok(reordered) => reordered,
2557+
Err(reason) => {
2558+
self.set_dynamic_send_reason(insn_id, reason);
2559+
self.push_insn_id(block, insn_id); continue;
2560+
}
2561+
}
2562+
} else {
2563+
args.clone()
2564+
};
2565+
2566+
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, state });
24352567
self.make_equal_to(insn_id, send_direct);
24362568
} else if def_type == VM_METHOD_TYPE_IVAR && args.is_empty() {
24372569
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state });
@@ -4477,7 +4609,6 @@ fn unhandled_call_type(flags: u32) -> Result<(), CallType> {
44774609

44784610
/// If a given call uses overly complex arguments, then we won't specialize.
44794611
fn unspecializable_call_type(flags: u32) -> bool {
4480-
((flags & VM_CALL_KWARG) != 0) ||
44814612
((flags & VM_CALL_ARGS_SPLAT) != 0) ||
44824613
((flags & VM_CALL_ARGS_BLOCKARG) != 0)
44834614
}

zjit/src/hir/opt_tests.rs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2761,9 +2761,9 @@ mod hir_opt_tests {
27612761
}
27622762

27632763
#[test]
2764-
fn dont_specialize_call_to_iseq_with_kw() {
2764+
fn specialize_call_to_iseq_with_required_kw() {
27652765
eval("
2766-
def foo(a:) = 1
2766+
def foo(a:) = a * 2
27672767
def test = foo(a: 1)
27682768
test
27692769
test
@@ -2779,7 +2779,35 @@ mod hir_opt_tests {
27792779
Jump bb2(v4)
27802780
bb2(v6:BasicObject):
27812781
v11:Fixnum[1] = Const Value(1)
2782-
IncrCounter complex_arg_pass_caller_kwarg
2782+
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
2783+
PatchPoint NoSingletonClass(Object@0x1000)
2784+
v20:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
2785+
v21:BasicObject = SendWithoutBlockDirect v20, :foo (0x1038), v11
2786+
CheckInterrupts
2787+
Return v21
2788+
");
2789+
}
2790+
2791+
#[test]
2792+
fn test_send_call_to_iseq_with_optional_kw() {
2793+
eval("
2794+
def foo(a: 1) = a
2795+
def test = foo(a: 2)
2796+
test
2797+
test
2798+
");
2799+
assert_snapshot!(hir_string("test"), @r"
2800+
fn test@<compiled>:3:
2801+
bb0():
2802+
EntryPoint interpreter
2803+
v1:BasicObject = LoadSelf
2804+
Jump bb2(v1)
2805+
bb1(v4:BasicObject):
2806+
EntryPoint JIT(0)
2807+
Jump bb2(v4)
2808+
bb2(v6:BasicObject):
2809+
v11:Fixnum[2] = Const Value(2)
2810+
IncrCounter complex_arg_pass_param_kw_opt
27832811
v13:BasicObject = SendWithoutBlock v6, :foo, v11
27842812
CheckInterrupts
27852813
Return v13
@@ -2805,7 +2833,7 @@ mod hir_opt_tests {
28052833
Jump bb2(v4)
28062834
bb2(v6:BasicObject):
28072835
v11:Fixnum[1] = Const Value(1)
2808-
IncrCounter complex_arg_pass_caller_kwarg
2836+
IncrCounter complex_arg_pass_param_kwrest
28092837
v13:BasicObject = SendWithoutBlock v6, :foo, v11
28102838
CheckInterrupts
28112839
Return v13
@@ -2830,7 +2858,7 @@ mod hir_opt_tests {
28302858
EntryPoint JIT(0)
28312859
Jump bb2(v4)
28322860
bb2(v6:BasicObject):
2833-
IncrCounter complex_arg_pass_param_kw
2861+
IncrCounter complex_arg_pass_param_kw_opt
28342862
v11:BasicObject = SendWithoutBlock v6, :foo
28352863
CheckInterrupts
28362864
Return v11
@@ -3116,7 +3144,7 @@ mod hir_opt_tests {
31163144
v13:NilClass = Const Value(nil)
31173145
PatchPoint MethodRedefined(Hash@0x1008, new@0x1010, cme:0x1018)
31183146
v46:HashExact = ObjectAllocClass Hash:VALUE(0x1008)
3119-
IncrCounter complex_arg_pass_param_kw
3147+
IncrCounter complex_arg_pass_param_kw_opt
31203148
IncrCounter complex_arg_pass_param_block
31213149
v20:BasicObject = SendWithoutBlock v46, :initialize
31223150
CheckInterrupts
@@ -7781,7 +7809,7 @@ mod hir_opt_tests {
77817809
bb2(v6:BasicObject):
77827810
v11:Fixnum[1] = Const Value(1)
77837811
IncrCounter complex_arg_pass_param_rest
7784-
IncrCounter complex_arg_pass_param_kw
7812+
IncrCounter complex_arg_pass_param_kw_opt
77857813
IncrCounter complex_arg_pass_param_kwrest
77867814
IncrCounter complex_arg_pass_param_block
77877815
v13:BasicObject = SendWithoutBlock v6, :fancy, v11

zjit/src/stats.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ make_counters! {
181181
send_fallback_too_many_args_for_lir,
182182
send_fallback_send_without_block_bop_redefined,
183183
send_fallback_send_without_block_operands_not_fixnum,
184+
send_fallback_send_without_block_direct_keyword_mismatch,
185+
send_fallback_send_without_block_direct_optional_keywords,
186+
send_fallback_send_without_block_direct_keyword_count_mismatch,
187+
send_fallback_send_without_block_direct_missing_keyword,
184188
send_fallback_send_polymorphic,
185189
send_fallback_send_megamorphic,
186190
send_fallback_send_no_profiles,
@@ -190,6 +194,8 @@ make_counters! {
190194
// The call has at least one feature on the caller or callee side
191195
// that the optimizer does not support.
192196
send_fallback_one_or_more_complex_arg_pass,
197+
// Caller has keyword arguments but callee doesn't expect them.
198+
send_fallback_unexpected_keyword_args,
193199
send_fallback_bmethod_non_iseq_proc,
194200
send_fallback_obj_to_string_not_string,
195201
send_fallback_send_cfunc_variadic,
@@ -279,7 +285,7 @@ make_counters! {
279285
// Unsupported parameter features
280286
complex_arg_pass_param_rest,
281287
complex_arg_pass_param_post,
282-
complex_arg_pass_param_kw,
288+
complex_arg_pass_param_kw_opt,
283289
complex_arg_pass_param_kwrest,
284290
complex_arg_pass_param_block,
285291
complex_arg_pass_param_forwardable,
@@ -476,12 +482,17 @@ pub fn send_fallback_counter(reason: crate::hir::SendFallbackReason) -> Counter
476482
TooManyArgsForLir => send_fallback_too_many_args_for_lir,
477483
SendWithoutBlockBopRedefined => send_fallback_send_without_block_bop_redefined,
478484
SendWithoutBlockOperandsNotFixnum => send_fallback_send_without_block_operands_not_fixnum,
485+
SendWithoutBlockDirectKeywordMismatch => send_fallback_send_without_block_direct_keyword_mismatch,
486+
SendWithoutBlockDirectOptionalKeywords => send_fallback_send_without_block_direct_optional_keywords,
487+
SendWithoutBlockDirectKeywordCountMismatch=> send_fallback_send_without_block_direct_keyword_count_mismatch,
488+
SendWithoutBlockDirectMissingKeyword => send_fallback_send_without_block_direct_missing_keyword,
479489
SendPolymorphic => send_fallback_send_polymorphic,
480490
SendMegamorphic => send_fallback_send_megamorphic,
481491
SendNoProfiles => send_fallback_send_no_profiles,
482492
SendCfuncVariadic => send_fallback_send_cfunc_variadic,
483493
SendCfuncArrayVariadic => send_fallback_send_cfunc_array_variadic,
484494
ComplexArgPass => send_fallback_one_or_more_complex_arg_pass,
495+
UnexpectedKeywordArgs => send_fallback_unexpected_keyword_args,
485496
ArgcParamMismatch => send_fallback_argc_param_mismatch,
486497
BmethodNonIseqProc => send_fallback_bmethod_non_iseq_proc,
487498
SendNotOptimizedMethodType(_) => send_fallback_send_not_optimized_method_type,

0 commit comments

Comments
 (0)