@@ -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.
44794611fn 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}
0 commit comments