Skip to content

Commit 9e7c11c

Browse files
committed
ZJIT: Handle caller_kwarg in direct send for only required kw params
1 parent 92580fa commit 9e7c11c

4 files changed

Lines changed: 159 additions & 18 deletions

File tree

zjit/src/codegen.rs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
381381
// Give up SendWithoutBlockDirect for 6+ args since asm.ccall() doesn't support it.
382382
Insn::SendWithoutBlockDirect { cd, state, args, .. } if args.len() + 1 > C_ARG_OPNDS.len() => // +1 for self
383383
gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::TooManyArgsForLir),
384-
Insn::SendWithoutBlockDirect { cme, iseq, recv, args, state, .. } => gen_send_without_block_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), &function.frame_state(*state)),
384+
Insn::SendWithoutBlockDirect { cme, iseq, recv, args, state, cd, kwarg, .. } => gen_send_without_block_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), &function.frame_state(*state), *cd, *kwarg),
385385
&Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
386386
&Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason),
387387
// Ensure we have enough room fit ec, self, and arguments
@@ -1229,9 +1229,72 @@ fn gen_send_without_block_direct(
12291229
recv: Opnd,
12301230
args: Vec<Opnd>,
12311231
state: &FrameState,
1232+
cd: *const rb_call_data,
1233+
kwarg: *const rb_callinfo_kwarg,
12321234
) -> lir::Opnd {
12331235
gen_incr_counter(asm, Counter::iseq_optimized_send_count);
12341236

1237+
// Handle keyword arguments if present.
1238+
let mut processed_args = args.clone();
1239+
if !kwarg.is_null() {
1240+
let callee_keyword = unsafe { get_iseq_body_param_keyword(iseq) };
1241+
if callee_keyword.is_null() {
1242+
// Caller is passing kwargs but callee doesn't expect them.
1243+
// Fall back to do the kwarg to hash conversion.
1244+
return gen_send_without_block(jit, asm, cd, state, SendFallbackReason::SendWithoutBlockDirectKeywordMismatch);
1245+
}
1246+
1247+
let caller_kw_count = unsafe { get_cikw_keyword_len(kwarg) } as usize;
1248+
let callee_kw_count = unsafe { (*callee_keyword).num } as usize;
1249+
let callee_kw_required = unsafe { (*callee_keyword).required_num } as usize;
1250+
let callee_kw_table = unsafe { (*callee_keyword).table };
1251+
1252+
// For now, only handle the case where all keywords are required.
1253+
if callee_kw_count != callee_kw_required {
1254+
return gen_send_without_block(jit, asm, cd, state, SendFallbackReason::SendWithoutBlockDirectOptionalKeywords);
1255+
}
1256+
if caller_kw_count != callee_kw_count {
1257+
return gen_send_without_block(jit, asm, cd, state, SendFallbackReason::SendWithoutBlockDirectKeywordCountMismatch);
1258+
}
1259+
1260+
// The keyword arguments are the last arguments in the args vector.
1261+
let kw_args_start = args.len() - caller_kw_count;
1262+
1263+
// Build a mapping from caller keywords to their positions.
1264+
let mut caller_kw_order: Vec<ID> = Vec::with_capacity(caller_kw_count);
1265+
for i in 0..caller_kw_count {
1266+
let sym = unsafe { get_cikw_keywords_idx(kwarg, i as i32) };
1267+
let id = unsafe { rb_sym2id(sym) };
1268+
caller_kw_order.push(id);
1269+
}
1270+
1271+
// Reorder keyword arguments to match callee expectation.
1272+
// See arg_setup_kw_parameters in vm_args.c.
1273+
let mut reordered_kw_args: Vec<Opnd> = Vec::with_capacity(callee_kw_count);
1274+
for i in 0..callee_kw_count {
1275+
let expected_id = unsafe { *callee_kw_table.add(i) };
1276+
1277+
// Find where this keyword is in the caller's order
1278+
let mut found = false;
1279+
for (j, &caller_id) in caller_kw_order.iter().enumerate() {
1280+
if caller_id == expected_id {
1281+
reordered_kw_args.push(args[kw_args_start + j]);
1282+
found = true;
1283+
break;
1284+
}
1285+
}
1286+
1287+
if !found {
1288+
// Required keyword not provided by caller which will raise an ArgumentError.
1289+
return gen_send_without_block(jit, asm, cd, state, SendFallbackReason::SendWithoutBlockDirectMissingKeyword);
1290+
}
1291+
}
1292+
1293+
// Replace the keyword arguments with the reordered ones.
1294+
processed_args.truncate(kw_args_start);
1295+
processed_args.extend(reordered_kw_args);
1296+
}
1297+
12351298
let local_size = unsafe { get_iseq_body_local_table_size(iseq) }.to_usize();
12361299
let stack_growth = state.stack_size() + local_size + unsafe { get_iseq_body_stack_max(iseq) }.to_usize();
12371300
gen_stack_overflow_check(jit, asm, state, stack_growth);
@@ -1279,9 +1342,9 @@ fn gen_send_without_block_direct(
12791342
asm.mov(CFP, new_cfp);
12801343
asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP);
12811344

1282-
// Set up arguments
1345+
// Set up arguments with possibly modified final_args.
12831346
let mut c_args = vec![recv];
1284-
c_args.extend(&args);
1347+
c_args.extend(&processed_args);
12851348

12861349
let num_optionals_passed = if unsafe { get_iseq_flags_has_opt(iseq) } {
12871350
// See vm_call_iseq_setup_normal_opt_start in vm_inshelper.c

zjit/src/hir.rs

Lines changed: 49 additions & 7 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,
@@ -830,6 +834,7 @@ pub enum Insn {
830834
iseq: IseqPtr,
831835
args: Vec<InsnId>,
832836
state: InsnId,
837+
kwarg: *const rb_callinfo_kwarg,
833838
},
834839

835840
// Invoke a builtin function
@@ -1122,11 +1127,14 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
11221127
}
11231128
Ok(())
11241129
}
1125-
Insn::SendWithoutBlockDirect { recv, cd, iseq, args, .. } => {
1130+
Insn::SendWithoutBlockDirect { recv, cd, iseq, args, kwarg, .. } => {
11261131
write!(f, "SendWithoutBlockDirect {recv}, :{} ({:?})", ruby_call_method_name(*cd), self.ptr_map.map_ptr(iseq))?;
11271132
for arg in args {
11281133
write!(f, ", {arg}")?;
11291134
}
1135+
if !kwarg.is_null() {
1136+
write!(f, " [kw]")?;
1137+
}
11301138
Ok(())
11311139
}
11321140
Insn::Send { recv, cd, args, blockiseq, .. } => {
@@ -1475,7 +1483,20 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq
14751483
use Counter::*;
14761484
if unsafe { rb_get_iseq_flags_has_rest(iseq) } { count_failure(complex_arg_pass_param_rest) }
14771485
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) }
1486+
1487+
// We support required-only keywords, but not optional keywords yet
1488+
if unsafe { rb_get_iseq_flags_has_kw(iseq) } {
1489+
let keyword = unsafe { get_iseq_body_param_keyword(iseq) };
1490+
if !keyword.is_null() {
1491+
let num = unsafe { (*keyword).num };
1492+
let required_num = unsafe { (*keyword).required_num };
1493+
// Only support required keywords for now (no optional keywords)
1494+
if num != required_num {
1495+
count_failure(complex_arg_pass_param_kw_opt)
1496+
}
1497+
}
1498+
}
1499+
14791500
if unsafe { rb_get_iseq_flags_has_kwrest(iseq) } { count_failure(complex_arg_pass_param_kwrest) }
14801501
if unsafe { rb_get_iseq_flags_has_block(iseq) } { count_failure(complex_arg_pass_param_block) }
14811502
if unsafe { rb_get_iseq_flags_forwardable(iseq) } { count_failure(complex_arg_pass_param_forwardable) }
@@ -1489,9 +1510,12 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq
14891510
// relies on rejecting features above.
14901511
let lead_num = unsafe { get_iseq_body_param_lead_num(iseq) };
14911512
let opt_num = unsafe { get_iseq_body_param_opt_num(iseq) };
1513+
let keyword = unsafe { get_iseq_body_param_keyword(iseq) };
1514+
let kw_req_num = if keyword.is_null() { 0 } else { unsafe { (*keyword).required_num } };
1515+
let req_num = lead_num + kw_req_num;
14921516
can_send = c_int::try_from(args.len())
14931517
.as_ref()
1494-
.map(|argc| (lead_num..=lead_num + opt_num).contains(argc))
1518+
.map(|argc| (req_num..=req_num + opt_num).contains(argc))
14951519
.unwrap_or(false);
14961520
if !can_send {
14971521
function.set_dynamic_send_reason(send_insn, ArgcParamMismatch);
@@ -1820,13 +1844,14 @@ impl Function {
18201844
state,
18211845
reason,
18221846
},
1823-
&SendWithoutBlockDirect { recv, cd, cme, iseq, ref args, state } => SendWithoutBlockDirect {
1847+
&SendWithoutBlockDirect { recv, cd, cme, iseq, ref args, state, kwarg } => SendWithoutBlockDirect {
18241848
recv: find!(recv),
18251849
cd,
18261850
cme,
18271851
iseq,
18281852
args: find_vec!(args),
18291853
state,
1854+
kwarg,
18301855
},
18311856
&Send { recv, cd, blockiseq, ref args, state, reason } => Send {
18321857
recv: find!(recv),
@@ -2396,7 +2421,16 @@ impl Function {
23962421
if let Some(profiled_type) = profiled_type {
23972422
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
23982423
}
2399-
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args, state });
2424+
2425+
// Check if caller is passing keywords but callee doesn't expect them.
2426+
let kwarg = unsafe { rb_vm_ci_kwarg(ci) };
2427+
if !kwarg.is_null() && !unsafe { rb_get_iseq_flags_has_kw(iseq) } {
2428+
// Caller has keywords but callee doesn't; Need to convert to hash.
2429+
self.set_dynamic_send_reason(insn_id, ComplexArgPass);
2430+
self.push_insn_id(block, insn_id); continue;
2431+
}
2432+
2433+
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args, state, kwarg });
24002434
self.make_equal_to(insn_id, send_direct);
24012435
} else if def_type == VM_METHOD_TYPE_BMETHOD {
24022436
let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) };
@@ -2431,7 +2465,16 @@ impl Function {
24312465
if let Some(profiled_type) = profiled_type {
24322466
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
24332467
}
2434-
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args, state });
2468+
2469+
// Check if caller is passing keywords but callee doesn't expect them.
2470+
let kwarg = unsafe { rb_vm_ci_kwarg(ci) };
2471+
if !kwarg.is_null() && !unsafe { rb_get_iseq_flags_has_kw(iseq) } {
2472+
// Caller has keywords but callee doesn't; Need to convert to hash.
2473+
self.set_dynamic_send_reason(insn_id, ComplexArgPass);
2474+
self.push_insn_id(block, insn_id); continue;
2475+
}
2476+
2477+
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args, state, kwarg });
24352478
self.make_equal_to(insn_id, send_direct);
24362479
} else if def_type == VM_METHOD_TYPE_IVAR && args.is_empty() {
24372480
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state });
@@ -4477,7 +4520,6 @@ fn unhandled_call_type(flags: u32) -> Result<(), CallType> {
44774520

44784521
/// If a given call uses overly complex arguments, then we won't specialize.
44794522
fn unspecializable_call_type(flags: u32) -> bool {
4480-
((flags & VM_CALL_KWARG) != 0) ||
44814523
((flags & VM_CALL_ARGS_SPLAT) != 0) ||
44824524
((flags & VM_CALL_ARGS_BLOCKARG) != 0)
44834525
}

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 [kw]
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: 9 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,
@@ -279,7 +283,7 @@ make_counters! {
279283
// Unsupported parameter features
280284
complex_arg_pass_param_rest,
281285
complex_arg_pass_param_post,
282-
complex_arg_pass_param_kw,
286+
complex_arg_pass_param_kw_opt,
283287
complex_arg_pass_param_kwrest,
284288
complex_arg_pass_param_block,
285289
complex_arg_pass_param_forwardable,
@@ -476,6 +480,10 @@ pub fn send_fallback_counter(reason: crate::hir::SendFallbackReason) -> Counter
476480
TooManyArgsForLir => send_fallback_too_many_args_for_lir,
477481
SendWithoutBlockBopRedefined => send_fallback_send_without_block_bop_redefined,
478482
SendWithoutBlockOperandsNotFixnum => send_fallback_send_without_block_operands_not_fixnum,
483+
SendWithoutBlockDirectKeywordMismatch => send_fallback_send_without_block_direct_keyword_mismatch,
484+
SendWithoutBlockDirectOptionalKeywords => send_fallback_send_without_block_direct_optional_keywords,
485+
SendWithoutBlockDirectKeywordCountMismatch=> send_fallback_send_without_block_direct_keyword_count_mismatch,
486+
SendWithoutBlockDirectMissingKeyword => send_fallback_send_without_block_direct_missing_keyword,
479487
SendPolymorphic => send_fallback_send_polymorphic,
480488
SendMegamorphic => send_fallback_send_megamorphic,
481489
SendNoProfiles => send_fallback_send_no_profiles,

0 commit comments

Comments
 (0)