Skip to content

Commit ee7257f

Browse files
committed
ZJIT: Handle caller_kwarg in direct send when all keyword params are required
1 parent aaeaa4d commit ee7257f

5 files changed

Lines changed: 353 additions & 19 deletions

File tree

test/ruby/test_zjit.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,15 @@ def entry = test(%(a\nb\nc))
615615
}, call_threshold: 2
616616
end
617617

618+
def test_send_kwarg_with_too_many_args_to_c_call
619+
assert_compiles '"a b c d {kwargs: :e}"', %q{
620+
def test(a:, b:, c:, d:, e:) = sprintf("%s %s %s %s %s", a, b, c, d, kwargs: e)
621+
def entry = test(e: :e, d: :d, c: :c, a: :a, b: :b)
622+
entry
623+
entry
624+
}, call_threshold: 2
625+
end
626+
618627
def test_send_kwrest
619628
assert_compiles '{a: 3}', %q{
620629
def test(**kwargs) = kwargs
@@ -633,6 +642,15 @@ def entry = test(1, c: 3)
633642
}, call_threshold: 2
634643
end
635644

645+
def test_send_req_opt_kwreq
646+
assert_compiles '[[1, 2, 3], [-1, -2, -3]]', %q{
647+
def test(a, b = 2, c:) = [a, b, c]
648+
def entry = [test(1, c: 3), test(-1, -2, c: -3)] # specify all, change kw order
649+
entry
650+
entry
651+
}, call_threshold: 2
652+
end
653+
636654
def test_send_req_opt_kwreq_kwopt
637655
assert_compiles '[[1, 2, 3, 4], [-1, -2, -3, -4]]', %q{
638656
def test(a, b = 2, c:, d: 4) = [a, b, c, d]

zjit/src/codegen.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
383383
&Insn::SendWithoutBlock { cd, state, reason, .. } => gen_send_without_block(jit, asm, cd, &function.frame_state(state), reason),
384384
// Give up SendWithoutBlockDirect for 6+ args since asm.ccall() doesn't support it.
385385
Insn::SendWithoutBlockDirect { cd, state, args, .. } if args.len() + 1 > C_ARG_OPNDS.len() => // +1 for self
386+
gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::TooManyArgsForLir),
387+
// Give up SendWithoutBlockDirect for 5+ args (plus 1 arg for keyword bits) since asm.ccall() doesn't support it.
388+
Insn::SendWithoutBlockDirect { cd, state, args, iseq, .. } if args.len() + 2 > C_ARG_OPNDS.len() && unsafe { rb_get_iseq_flags_has_kw(*iseq) } => // +1 for self +1 for keyword bits
389+
386390
gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::TooManyArgsForLir),
387391
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)),
388392
&Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
@@ -1350,13 +1354,23 @@ fn gen_send_without_block_direct(
13501354
let mut c_args = vec![recv];
13511355
c_args.extend(&args);
13521356

1357+
if unsafe { rb_get_iseq_flags_has_kw(iseq) } {
1358+
// Currently we only get to this point if all the accepted keyword args are required.
1359+
let unspecified_bits = 0;
1360+
// For each optional keyword that isn't passed we would `unspecified_bits |= (0x01 << idx)`.
1361+
c_args.push(VALUE::fixnum_from_usize(unspecified_bits).into());
1362+
}
1363+
13531364
let params = unsafe { iseq.params() };
13541365
let num_optionals_passed = if params.flags.has_opt() != 0 {
13551366
// See vm_call_iseq_setup_normal_opt_start in vm_inshelper.c
13561367
let lead_num = params.lead_num as u32;
13571368
let opt_num = params.opt_num as u32;
1358-
assert!(args.len() as u32 <= lead_num + opt_num);
1359-
let num_optionals_passed = args.len() as u32 - lead_num;
1369+
let keyword = params.keyword;
1370+
let kw_req_num = if keyword.is_null() { 0 } else { unsafe { (*keyword).required_num } } as u32;
1371+
let req_num = lead_num + kw_req_num;
1372+
assert!(args.len() as u32 <= req_num + opt_num);
1373+
let num_optionals_passed = args.len() as u32 - req_num;
13601374
num_optionals_passed
13611375
} else {
13621376
0

zjit/src/hir.rs

Lines changed: 143 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,10 @@ pub enum SendFallbackReason {
615615
SendWithoutBlockNotOptimizedMethodTypeOptimized(OptimizedMethodType),
616616
SendWithoutBlockBopRedefined,
617617
SendWithoutBlockOperandsNotFixnum,
618+
SendWithoutBlockDirectKeywordMismatch,
619+
SendWithoutBlockDirectOptionalKeywords,
620+
SendWithoutBlockDirectKeywordCountMismatch,
621+
SendWithoutBlockDirectMissingKeyword,
618622
SendPolymorphic,
619623
SendMegamorphic,
620624
SendNoProfiles,
@@ -631,6 +635,8 @@ pub enum SendFallbackReason {
631635
/// The call has at least one feature on the caller or callee side that the optimizer does not
632636
/// support.
633637
ComplexArgPass,
638+
/// Caller has keyword arguments but callee doesn't expect them; need to convert to hash.
639+
UnexpectedKeywordArgs,
634640
/// Initial fallback reason for every instruction, which should be mutated to
635641
/// a more actionable reason when an attempt to specialize the instruction fails.
636642
Uncategorized(ruby_vminsn_type),
@@ -1528,7 +1534,19 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq
15281534
use Counter::*;
15291535
if 0 != params.flags.has_rest() { count_failure(complex_arg_pass_param_rest) }
15301536
if 0 != params.flags.has_post() { count_failure(complex_arg_pass_param_post) }
1531-
if 0 != params.flags.has_kw() { count_failure(complex_arg_pass_param_kw) }
1537+
1538+
if 0 != params.flags.has_kw() {
1539+
let keyword = params.keyword;
1540+
if !keyword.is_null() {
1541+
let num = unsafe { (*keyword).num };
1542+
let required_num = unsafe { (*keyword).required_num };
1543+
// Only support required keywords for now (no optional keywords)
1544+
if num != required_num {
1545+
count_failure(complex_arg_pass_param_kw_opt)
1546+
}
1547+
}
1548+
}
1549+
15321550
if 0 != params.flags.has_kwrest() { count_failure(complex_arg_pass_param_kwrest) }
15331551
if 0 != params.flags.has_block() { count_failure(complex_arg_pass_param_block) }
15341552
if 0 != params.flags.forwardable() { count_failure(complex_arg_pass_param_forwardable) }
@@ -1541,9 +1559,12 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq
15411559
// Because we exclude e.g. post parameters above, they are also excluded from the sum below.
15421560
let lead_num = params.lead_num;
15431561
let opt_num = params.opt_num;
1562+
let keyword = params.keyword;
1563+
let kw_req_num = if keyword.is_null() { 0 } else { unsafe { (*keyword).required_num } };
1564+
let req_num = lead_num + kw_req_num;
15441565
can_send = c_int::try_from(args.len())
15451566
.as_ref()
1546-
.map(|argc| (lead_num..=lead_num + opt_num).contains(argc))
1567+
.map(|argc| (req_num..=req_num + opt_num).contains(argc))
15471568
.unwrap_or(false);
15481569
if !can_send {
15491570
function.set_dynamic_send_reason(send_insn, ArgcParamMismatch);
@@ -2255,6 +2276,72 @@ impl Function {
22552276
}
22562277
}
22572278

2279+
/// Reorder keyword arguments to match the callee's expectation.
2280+
///
2281+
/// Returns Ok with reordered arguments if successful, or Err with the fallback reason if not.
2282+
fn reorder_keyword_arguments(
2283+
&self,
2284+
args: &[InsnId],
2285+
kwarg: *const rb_callinfo_kwarg,
2286+
iseq: IseqPtr,
2287+
) -> Result<Vec<InsnId>, SendFallbackReason> {
2288+
let callee_keyword = unsafe { rb_get_iseq_body_param_keyword(iseq) };
2289+
if callee_keyword.is_null() {
2290+
// Caller is passing kwargs but callee doesn't expect them.
2291+
return Err(SendWithoutBlockDirectKeywordMismatch);
2292+
}
2293+
2294+
let caller_kw_count = unsafe { get_cikw_keyword_len(kwarg) } as usize;
2295+
let callee_kw_count = unsafe { (*callee_keyword).num } as usize;
2296+
let callee_kw_required = unsafe { (*callee_keyword).required_num } as usize;
2297+
let callee_kw_table = unsafe { (*callee_keyword).table };
2298+
2299+
// For now, only handle the case where all keywords are required.
2300+
if callee_kw_count != callee_kw_required {
2301+
return Err(SendWithoutBlockDirectOptionalKeywords);
2302+
}
2303+
if caller_kw_count != callee_kw_count {
2304+
return Err(SendWithoutBlockDirectKeywordCountMismatch);
2305+
}
2306+
2307+
// The keyword arguments are the last arguments in the args vector.
2308+
let kw_args_start = args.len() - caller_kw_count;
2309+
2310+
// Build a mapping from caller keywords to their positions.
2311+
let mut caller_kw_order: Vec<ID> = Vec::with_capacity(caller_kw_count);
2312+
for i in 0..caller_kw_count {
2313+
let sym = unsafe { get_cikw_keywords_idx(kwarg, i as i32) };
2314+
let id = unsafe { rb_sym2id(sym) };
2315+
caller_kw_order.push(id);
2316+
}
2317+
2318+
// Reorder keyword arguments to match callee expectation.
2319+
let mut reordered_kw_args: Vec<InsnId> = Vec::with_capacity(callee_kw_count);
2320+
for i in 0..callee_kw_count {
2321+
let expected_id = unsafe { *callee_kw_table.add(i) };
2322+
2323+
// Find where this keyword is in the caller's order
2324+
let mut found = false;
2325+
for (j, &caller_id) in caller_kw_order.iter().enumerate() {
2326+
if caller_id == expected_id {
2327+
reordered_kw_args.push(args[kw_args_start + j]);
2328+
found = true;
2329+
break;
2330+
}
2331+
}
2332+
2333+
if !found {
2334+
// Required keyword not provided by caller which will raise an ArgumentError.
2335+
return Err(SendWithoutBlockDirectMissingKeyword);
2336+
}
2337+
}
2338+
2339+
// Replace the keyword arguments with the reordered ones.
2340+
let mut processed_args = args[..kw_args_start].to_vec();
2341+
processed_args.extend(reordered_kw_args);
2342+
Ok(processed_args)
2343+
}
2344+
22582345
/// Resolve the receiver type for method dispatch optimization.
22592346
///
22602347
/// Takes the receiver's Type, receiver HIR instruction, and ISEQ instruction index.
@@ -2470,6 +2557,7 @@ impl Function {
24702557
cme = unsafe { rb_aliased_callable_method_entry(cme) };
24712558
def_type = unsafe { get_cme_def_type(cme) };
24722559
}
2560+
24732561
if def_type == VM_METHOD_TYPE_ISEQ {
24742562
// TODO(max): Allow non-iseq; cache cme
24752563
// Only specialize positional-positional calls
@@ -2485,7 +2573,29 @@ impl Function {
24852573
if let Some(profiled_type) = profiled_type {
24862574
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
24872575
}
2488-
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args, state });
2576+
2577+
// Check if caller is passing keywords but callee doesn't expect them.
2578+
let kwarg = unsafe { rb_vm_ci_kwarg(ci) };
2579+
if !kwarg.is_null() && !unsafe { rb_get_iseq_flags_has_kw(iseq) } {
2580+
// Caller has keywords but callee doesn't; Need to convert to hash.
2581+
self.set_dynamic_send_reason(insn_id, UnexpectedKeywordArgs);
2582+
self.push_insn_id(block, insn_id); continue;
2583+
}
2584+
2585+
// Handle keyword argument reordering if present.
2586+
let processed_args = if !kwarg.is_null() {
2587+
match self.reorder_keyword_arguments(&args, kwarg, iseq) {
2588+
Ok(reordered) => reordered,
2589+
Err(reason) => {
2590+
self.set_dynamic_send_reason(insn_id, reason);
2591+
self.push_insn_id(block, insn_id); continue;
2592+
}
2593+
}
2594+
} else {
2595+
args.clone()
2596+
};
2597+
2598+
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, state });
24892599
self.make_equal_to(insn_id, send_direct);
24902600
} else if def_type == VM_METHOD_TYPE_BMETHOD {
24912601
let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) };
@@ -2520,7 +2630,29 @@ impl Function {
25202630
if let Some(profiled_type) = profiled_type {
25212631
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
25222632
}
2523-
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args, state });
2633+
2634+
// Check if caller is passing keywords but callee doesn't expect them.
2635+
let kwarg = unsafe { rb_vm_ci_kwarg(ci) };
2636+
if !kwarg.is_null() && !unsafe { rb_get_iseq_flags_has_kw(iseq) } {
2637+
// Caller has keywords but callee doesn't; Need to convert to hash.
2638+
self.set_dynamic_send_reason(insn_id, UnexpectedKeywordArgs);
2639+
self.push_insn_id(block, insn_id); continue;
2640+
}
2641+
2642+
// Handle keyword argument reordering if present.
2643+
let processed_args = if !kwarg.is_null() {
2644+
match self.reorder_keyword_arguments(&args, kwarg, iseq) {
2645+
Ok(reordered) => reordered,
2646+
Err(reason) => {
2647+
self.set_dynamic_send_reason(insn_id, reason);
2648+
self.push_insn_id(block, insn_id); continue;
2649+
}
2650+
}
2651+
} else {
2652+
args.clone()
2653+
};
2654+
2655+
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, state });
25242656
self.make_equal_to(insn_id, send_direct);
25252657
} else if def_type == VM_METHOD_TYPE_IVAR && args.is_empty() {
25262658
// Check if we're accessing ivars of a Class or Module object as they require single-ractor mode.
@@ -3062,7 +3194,7 @@ impl Function {
30623194

30633195
// When seeing &block argument, fall back to dynamic dispatch for now
30643196
// TODO: Support block forwarding
3065-
if unspecializable_call_type(ci_flags) {
3197+
if unspecializable_c_call_type(ci_flags) {
30663198
fun.count_complex_call_features(block, ci_flags);
30673199
fun.set_dynamic_send_reason(send_insn_id, ComplexArgPass);
30683200
return Err(());
@@ -4960,9 +5092,14 @@ fn unhandled_call_type(flags: u32) -> Result<(), CallType> {
49605092
Ok(())
49615093
}
49625094

5095+
/// If a given call to a c func uses overly complex arguments, then we won't specialize.
5096+
fn unspecializable_c_call_type(flags: u32) -> bool {
5097+
((flags & VM_CALL_KWARG) != 0) ||
5098+
unspecializable_call_type(flags)
5099+
}
5100+
49635101
/// If a given call uses overly complex arguments, then we won't specialize.
49645102
fn unspecializable_call_type(flags: u32) -> bool {
4965-
((flags & VM_CALL_KWARG) != 0) ||
49665103
((flags & VM_CALL_ARGS_SPLAT) != 0) ||
49675104
((flags & VM_CALL_ARGS_BLOCKARG) != 0)
49685105
}

0 commit comments

Comments
 (0)