@@ -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.
49645102fn 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