@@ -1018,6 +1018,10 @@ pub struct CCallWithFrameData {
10181018 pub return_type: Type,
10191019 pub elidable: bool,
10201020 pub block: Option<BlockHandler>,
1021+ /// See [`SendDirectData::block_arg`]. Unlike the ISEQ case this is *not* taken off the VM
1022+ /// stack: the frame the C method runs in is pushed over the argument slots, so the frame
1023+ /// setup counts the block argument's slot even though the C function never sees it.
1024+ pub block_arg: Option<InsnId>,
10211025}
10221026
10231027/// Payload of [`Insn::SendDirect`]. Boxed in the enum to keep `Insn` small.
@@ -1051,6 +1055,8 @@ pub struct CCallVariadicData {
10511055 pub return_type: Type,
10521056 pub elidable: bool,
10531057 pub block: Option<BlockHandler>,
1058+ /// See [`CCallWithFrameData::block_arg`].
1059+ pub block_arg: Option<InsnId>,
10541060}
10551061
10561062/// An instruction in the SSA IR. The output of an instruction is referred to by the index of
@@ -1700,11 +1706,13 @@ macro_rules! for_each_operand_impl {
17001706 Insn::CCallWithFrame(insn) => {
17011707 $visit_one!(insn.recv);
17021708 $visit_many!(insn.args);
1709+ $visit_many!(insn.block_arg);
17031710 $visit_one!(insn.state);
17041711 }
17051712 Insn::CCallVariadic(insn) => {
17061713 $visit_one!(insn.recv);
17071714 $visit_many!(insn.args);
1715+ $visit_many!(insn.block_arg);
17081716 $visit_one!(insn.state);
17091717 }
17101718 Insn::InvokeBlock { args, state, .. } => {
@@ -2483,7 +2491,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
24832491 Ok(())
24842492 },
24852493 Insn::CCallWithFrame(insn) => {
2486- let CCallWithFrameData { cfunc, recv, args, name, cme, block, .. } = &**insn;
2494+ let CCallWithFrameData { cfunc, recv, args, name, cme, block, block_arg, .. } = &**insn;
24872495 write!(f, "CCallWithFrame {recv}, :{}@{:p}", qualified_method_name(unsafe { (**cme).owner }, *name), self.ptr_map.map_ptr(*cfunc))?;
24882496 write_separated!(f, ", ", ", ", args);
24892497 match block {
@@ -2493,12 +2501,18 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
24932501 write!(f, ", block=&block")?,
24942502 None => {}
24952503 }
2504+ if let Some(block_arg) = block_arg {
2505+ write!(f, ", block=&{block_arg}")?;
2506+ }
24962507 Ok(())
24972508 },
24982509 Insn::CCallVariadic(insn) => {
2499- let CCallVariadicData { cfunc, recv, args, name, cme, .. } = &**insn;
2510+ let CCallVariadicData { cfunc, recv, args, name, cme, block_arg, .. } = &**insn;
25002511 write!(f, "CCallVariadic {recv}, :{}@{:p}", qualified_method_name(unsafe { (**cme).owner }, *name), self.ptr_map.map_ptr(*cfunc))?;
25012512 write_separated!(f, ", ", ", ", args);
2513+ if let Some(block_arg) = block_arg {
2514+ write!(f, ", block=&{block_arg}")?;
2515+ }
25022516 Ok(())
25032517 },
25042518 Insn::IncrCounterPtr { .. } => write!(f, "IncrCounterPtr"),
@@ -5883,7 +5897,12 @@ impl Function {
58835897 }
58845898 let mut stripped_block_arg = false;
58855899 let mut send_block_arg = None;
5886- if send_block == Some(BlockHandler::BlockArg) && def_type == VM_METHOD_TYPE_ISEQ {
5900+ // A C method's frame carries the block handler in its specval just like an
5901+ // ISEQ frame's, so the same reduction applies; the difference is that the
5902+ // block argument keeps its VM stack slot, which the C frame setup accounts
5903+ // for. Nothing else reads `args` positionally for a C call.
5904+ if send_block == Some(BlockHandler::BlockArg)
5905+ && matches!(def_type, VM_METHOD_TYPE_ISEQ | VM_METHOD_TYPE_CFUNC) {
58875906 // The block arg is the last element in args
58885907 if let Some(&block_arg) = args.last() {
58895908 let statically_nil = self.is_a(block_arg, types::NilClass);
@@ -6245,12 +6264,13 @@ impl Function {
62456264 cme: *const rb_callable_method_entry_struct,
62466265 method_id: ID,
62476266 argc: u32,
6267+ // The call site's flags with `VM_CALL_ARGS_BLOCKARG` cleared when
6268+ // `block_arg` already holds the handler the interpreter would have
6269+ // built from it.
6270+ ci_flags: u32,
6271+ block_arg: Option<InsnId>,
62486272 ) -> Result<(), ()> {
6249- let call_info = unsafe { (*cd).ci };
6250-
6251- let ci_flags = unsafe { vm_ci_flag(call_info) };
6252- // When seeing &block argument, fall back to dynamic dispatch for now
6253- // TODO: Support block forwarding
6273+ // Argument shapes the C frame setup cannot reproduce.
62546274 if unspecializable_c_call_type(ci_flags) {
62556275 // Only count features NOT already counted in type_specialize.
62566276 if !unspecializable_call_type(ci_flags) {
@@ -6265,6 +6285,10 @@ impl Function {
62656285 Some(BlockHandler::BlockIseq(blockiseq)) => Some(blockiseq),
62666286 None => None,
62676287 };
6288+ // A block reaches the callee either way, so neither the inline
6289+ // bodies nor the leaf fast path (which push no frame to carry the
6290+ // handler) can serve this call.
6291+ let passes_block = blockiseq.is_some() || block_arg.is_some();
62686292
62696293 let cfunc = unsafe { get_cme_def_body_cfunc(cme) };
62706294 // Find the `argc` (arity) of the C method, which describes the parameters it expects
@@ -6279,10 +6303,8 @@ impl Function {
62796303 }
62806304 let props = props.unwrap_or_default();
62816305 let return_type = props.return_type;
6282- let elidable = match blockiseq {
6283- Some(_) => false, // Don't consider cfuncs with block arguments as elidable for now
6284- None => props.elidable,
6285- };
6306+ // Don't consider cfuncs with block arguments as elidable for now
6307+ let elidable = !passes_block && props.elidable;
62866308
62876309 match cfunc_argc {
62886310 0.. => {
@@ -6309,7 +6331,7 @@ impl Function {
63096331 }
63106332
63116333 // Try inlining the cfunc into HIR. Only inline if we don't have a block argument
6312- if blockiseq.is_none() {
6334+ if !passes_block {
63136335 let tmp_block = fun.new_block(u32::MAX);
63146336 if let Some(replacement) = (props.inline)(fun, tmp_block, recv, &args, state) {
63156337 // Copy contents of tmp_block to block
@@ -6352,6 +6374,7 @@ impl Function {
63526374 return_type,
63536375 elidable,
63546376 block: blockiseq.map(BlockHandler::BlockIseq),
6377+ block_arg,
63556378 })));
63566379 fun.insn_types[ccall] = fun.infer_type(ccall);
63576380 fun.make_equal_to(send_insn_id, ccall);
@@ -6376,7 +6399,7 @@ impl Function {
63766399 }
63776400
63786401 // Try inlining the cfunc into HIR. Only inline if we don't have a block argument
6379- if blockiseq.is_none() {
6402+ if !passes_block {
63806403 let tmp_block = fun.new_block(u32::MAX);
63816404 if let Some(replacement) = (props.inline)(fun, tmp_block, recv, &args, state) {
63826405 // Copy contents of tmp_block to block
@@ -6419,6 +6442,7 @@ impl Function {
64196442 return_type,
64206443 elidable,
64216444 block: blockiseq.map(BlockHandler::BlockIseq),
6445+ block_arg,
64226446 })));
64236447 fun.insn_types[ccall] = fun.infer_type(ccall);
64246448 fun.make_equal_to(send_insn_id, ccall);
@@ -6434,7 +6458,7 @@ impl Function {
64346458 }
64356459
64366460 let ccall_argc = if send_mid_override.is_some() { args.len() as u32 } else { unsafe { vm_ci_argc(ci) } };
6437- if reduce_send_to_ccall(self, block, insn_id, recv, cd, send_block, args, state, klass, profiled_type, cme, mid, ccall_argc).is_ok() {
6461+ if reduce_send_to_ccall(self, block, insn_id, recv, cd, send_block, args, state, klass, profiled_type, cme, mid, ccall_argc, flags_for_check, send_block_arg ).is_ok() {
64386462 continue;
64396463 }
64406464
@@ -6694,6 +6718,7 @@ impl Function {
66946718 return_type,
66956719 elidable,
66966720 block: None,
6721+ block_arg: None,
66976722 })))
66986723 };
66996724 self.make_equal_to(insn_id, ccall);
@@ -6743,6 +6768,7 @@ impl Function {
67436768 return_type,
67446769 elidable,
67456770 block: None,
6771+ block_arg: None,
67466772 })))
67476773 };
67486774 self.make_equal_to(insn_id, ccall);
@@ -9135,13 +9161,19 @@ impl Function {
91359161 for &arg in &insn.args {
91369162 self.assert_subtype(insn_id, arg, types::BasicObject)?;
91379163 }
9164+ if let Some(block_arg) = insn.block_arg {
9165+ self.assert_subtype(insn_id, block_arg, types::BasicObject)?;
9166+ }
91389167 Ok(())
91399168 }
91409169 Insn::CCallVariadic(ref insn) => {
91419170 self.assert_subtype(insn_id, insn.recv, types::BasicObject)?;
91429171 for &arg in &insn.args {
91439172 self.assert_subtype(insn_id, arg, types::BasicObject)?;
91449173 }
9174+ if let Some(block_arg) = insn.block_arg {
9175+ self.assert_subtype(insn_id, block_arg, types::BasicObject)?;
9176+ }
91459177 Ok(())
91469178 }
91479179 Insn::ArrayPackBuffer { ref elements, fmt, buffer, .. } => {
@@ -10132,9 +10164,10 @@ struct AddIseqResult {
1013210164 profiles: ProfileOracle,
1013310165}
1013410166
10135- /// Whether any receiver class this site profiled resolves the call to an ISEQ method, which is
10136- /// the only method type whose frame setup `type_specialize` can hand a `&blk` block handler to.
10137- fn profiled_recv_has_iseq_callee(
10167+ /// Whether any receiver class this site profiled resolves the call to a method whose frame setup
10168+ /// `type_specialize` can hand a `&blk` block handler to. Only ISEQ and C methods get such a
10169+ /// frame; the rest keep the dynamic send whatever the block argument is.
10170+ fn profiled_recv_takes_block_handler(
1013810171 fun: &Function,
1013910172 profiles: &ProfileOracle,
1014010173 recv: InsnId,
@@ -10151,7 +10184,7 @@ fn profiled_recv_has_iseq_callee(
1015110184 cme = unsafe { rb_aliased_callable_method_entry(cme) };
1015210185 def_type = unsafe { get_cme_def_type(cme) };
1015310186 }
10154- def_type == VM_METHOD_TYPE_ISEQ
10187+ matches!( def_type, VM_METHOD_TYPE_ISEQ | VM_METHOD_TYPE_CFUNC)
1015510188 })
1015610189}
1015710190
@@ -11699,10 +11732,9 @@ fn add_iseq_to_hir(
1169911732 && args.last().is_some_and(|arg| block_param_proxy_values.contains(arg))
1170011733 && block_arg_summary.as_ref().is_some_and(|summary| summary.buckets().iter().any(|profiled_type|
1170111734 !profiled_type.is_empty() && profiled_type.class() == proxy_class))
11702- // Only an ISEQ callee's frame setup takes the handler; a C method reads
11703- // its block from a frame ZJIT does not build for a `&blk` argument, so
11704- // its call stays dynamic and the branch would be dead weight.
11705- && profiled_recv_has_iseq_callee(fun, &profiles, recv, exit_id, cd);
11735+ // Only an ISEQ or C callee's frame setup takes the handler; for anything
11736+ // else the call stays dynamic and the branch would be dead weight.
11737+ && profiled_recv_takes_block_handler(fun, &profiles, recv, exit_id, cd);
1170611738 let proxy_join = if proxy_split {
1170711739 let block_arg_insn = *args.last().unwrap();
1170811740 let join_block = fun.new_block(insn_idx);
0 commit comments