From 258c6b0bfc6d0c790a7ee176d478cc4f3edde70e Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 30 Mar 2026 11:57:39 -0700 Subject: [PATCH 1/7] Update debug.gem sha to fix random failures https://github.com/ruby/debug/pull/1174 --- gems/bundled_gems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/bundled_gems b/gems/bundled_gems index d68c27f01eaa0b..9d3fcd095e6941 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -18,7 +18,7 @@ matrix 0.4.3 https://github.com/ruby/matrix prime 0.1.4 https://github.com/ruby/prime rbs 4.0.2 https://github.com/ruby/rbs typeprof 0.31.1 https://github.com/ruby/typeprof -debug 1.11.1 https://github.com/ruby/debug +debug 1.11.1 https://github.com/ruby/debug 2897edad6d2c2eeb49ffe915192c54572dbe6c82 racc 1.8.1 https://github.com/ruby/racc mutex_m 0.3.0 https://github.com/ruby/mutex_m getoptlong 0.2.1 https://github.com/ruby/getoptlong From 714b4b5489386a2d9b797c7fa4e8a060ad8124d0 Mon Sep 17 00:00:00 2001 From: Nozomi Hijikata <121233810+nozomemein@users.noreply.github.com> Date: Tue, 31 Mar 2026 04:26:35 +0900 Subject: [PATCH 2/7] ZJIT: Compile modified path in getblockparamproxy (#16573) ZJIT: Avoid side exits on modified getblockparamproxy --- zjit/src/codegen_tests.rs | 47 +++-- zjit/src/hir.rs | 91 +++++++--- zjit/src/hir/opt_tests.rs | 213 ++++++++++++++++++---- zjit/src/hir/tests.rs | 365 +++++++++++++++++++++++++++++++------- zjit/src/stats.rs | 2 - 5 files changed, 563 insertions(+), 155 deletions(-) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index db94a32036ad4e..ee4966d19fe68b 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -420,6 +420,33 @@ fn test_getblockparamproxy() { assert_snapshot!(assert_compiles("test { 1 }"), @"1"); } +#[test] +fn test_getblockparamproxy_modified() { + eval(" + def test(&block) + b = block + 0.then(&block) + end + test { 1 } + "); + assert_contains_opcode("test", YARVINSN_getblockparamproxy); + assert_snapshot!(inspect("test { 1 }"), @"1"); +} + +#[test] +fn test_getblockparamproxy_modified_nested_block() { + eval(" + def test(&block) + proc do + b = block + 0.then(&block) + end + end + test { 1 }.call + "); + assert_snapshot!(inspect("test { 1 }.call"), @"1"); +} + #[test] fn test_getblockparam() { eval(" @@ -462,9 +489,7 @@ fn test_setblockparam_nested_block() { } #[test] -fn test_setblockparam_side_exit() { - // This pattern side exits because `block.call` goes through - // getblockparamproxy's modified-block-parameter case. +fn test_getblockparamproxy_after_setblockparam() { eval(" def test(&block) block = proc { 3 } @@ -473,21 +498,7 @@ fn test_setblockparam_side_exit() { test { 1 } "); assert_contains_opcode("test", YARVINSN_setblockparam); - assert_snapshot!(inspect("test { 1 }"), @"3"); -} - -#[test] -fn test_getblockparam_proxy_side_exit_restores_block_local() { - eval(" - def test(&block) - b = block - raise \"test\" unless block - b ? 2 : 3 - end - test {} - "); - assert_contains_opcode("test", YARVINSN_getblockparam); - assert_snapshot!(assert_compiles("test {}"), @"2"); + assert_snapshot!(assert_compiles("test { 1 }"), @"3"); } #[test] diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index c50ba467750811..9c1f0d84a1db1d 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -524,7 +524,6 @@ pub enum SideExitReason { CalleeSideExit, ObjToStringFallback, Interrupt, - BlockParamProxyModified, BlockParamProxyNotIseqOrIfunc, BlockParamProxyNotNil, BlockParamWbRequired, @@ -2611,6 +2610,24 @@ impl Function { id } + fn new_branch_block( + &mut self, + insn_idx: u32, + exit_state: &FrameState, + locals_count: usize, + stack_count: usize, + ) -> (BlockId, InsnId, FrameState, InsnId) { + let block = self.new_block(insn_idx); + let self_param = self.push_insn(block, Insn::Param); + let mut state = exit_state.clone(); + state.locals.clear(); + state.stack.clear(); + state.locals.extend((0..locals_count).map(|_| self.push_insn(block, Insn::Param))); + state.stack.extend((0..stack_count).map(|_| self.push_insn(block, Insn::Param))); + let snapshot = self.push_insn(block, Insn::Snapshot { state: state.clone() }); + (block, self_param, state, snapshot) + } + fn remove_block(&mut self, block_id: BlockId) { if BlockId(self.blocks.len() - 1) != block_id { panic!("Can only remove the last block"); @@ -7578,7 +7595,9 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { }); } YARVINSN_getblockparamproxy => { + let ep_offset = get_arg(pc, 0).as_u32(); let level = get_arg(pc, 1).as_u32(); + let branch_insn_idx = exit_state.insn_idx as u32; let profiled_block_type = if let Some([block_handler_distribution]) = profiles.payload.profile.get_operand_types(exit_state.insn_idx) { let summary = TypeDistributionSummary::new(block_handler_distribution); @@ -7587,16 +7606,44 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { None }; + let locals_count = state.locals.len(); + let stack_count = state.stack.len(); + let entry_args = state.as_args(self_param); + + // `getblockparamproxy` has two semantic paths: + // - modified: return the already-materialized block local from EP + // - unmodified: inspect the block handler and produce proxy/nil + let (modified_block, modified_self_param, mut modified_state, ..) = + fun.new_branch_block(branch_insn_idx, &exit_state, locals_count, stack_count); + let (unmodified_block, unmodified_self_param, mut unmodified_state, unmodified_exit_id) = + fun.new_branch_block(branch_insn_idx, &exit_state, locals_count, stack_count); + let join_block = insn_idx_to_block.get(&insn_idx).copied().unwrap_or_else(|| fun.new_block(insn_idx)); + let ep = fun.push_insn(block, Insn::GetEP { level }); - let flags = fun.push_insn(block, Insn::LoadField { recv: ep, id: ID!(_env_data_index_flags), offset: SIZEOF_VALUE_I32 * (VM_ENV_DATA_INDEX_FLAGS as i32), return_type: types::CInt64 }); - fun.push_insn(block, Insn::GuardNoBitsSet { val: flags, mask: Const::CUInt64(VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM.into()), mask_name: Some(ID!(VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM)), reason: SideExitReason::BlockParamProxyModified, state: exit_id }); + let is_modified = fun.push_insn(block, Insn::IsBlockParamModified { ep }); + + fun.push_insn(block, Insn::IfTrue { val: is_modified, target: BranchEdge { target: modified_block, args: entry_args.clone() }}); + fun.push_insn(block, Insn::Jump(BranchEdge { target: unmodified_block, args: entry_args })); + + // Push modified block: load the block local via EP. + let ep = fun.push_insn(modified_block, Insn::GetEP { level }); + let modified_val = fun.get_local_from_ep(modified_block, ep, ep_offset, level, types::BasicObject); + if level == 0 { + modified_state.setlocal(ep_offset, modified_val); + } + modified_state.stack_push(modified_val); + fun.push_insn(modified_block, Insn::Jump(BranchEdge { target: join_block, args: modified_state.as_args(modified_self_param) })); - let block_handler = fun.push_insn(block, Insn::LoadField { recv: ep, id: ID!(_env_data_index_specval), offset: SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL, return_type: types::CInt64 }); + // Push unmodified block: inspect the current block handler to + // decide whether this path returns `nil` or `BlockParamProxy`. + let ep = fun.push_insn(unmodified_block, Insn::GetEP { level }); + let block_handler = fun.push_insn(unmodified_block, Insn::LoadField { recv: ep, id: ID!(_env_data_index_specval), offset: SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL, return_type: types::CInt64 }); match profiled_block_type { Some(ty) if ty.nil_p() => { - fun.push_insn(block, Insn::GuardBitEquals { val: block_handler, expected: Const::CInt64(VM_BLOCK_HANDLER_NONE.into()), reason: SideExitReason::BlockParamProxyNotNil, state: exit_id }); - state.stack_push(fun.push_insn(block, Insn::Const { val: Const::Value(Qnil) })); + fun.push_insn(unmodified_block, Insn::GuardBitEquals { val: block_handler, expected: Const::CInt64(VM_BLOCK_HANDLER_NONE.into()), reason: SideExitReason::BlockParamProxyNotNil, state: unmodified_exit_id }); + unmodified_state.stack_push(fun.push_insn(unmodified_block, Insn::Const { val: Const::Value(Qnil) })); + fun.push_insn(unmodified_block, Insn::Jump(BranchEdge { target: join_block, args: unmodified_state.as_args(unmodified_self_param) })); } _ => { // This handles two cases which are nearly identical @@ -7607,31 +7654,19 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { const _: () = assert!(RUBY_SYMBOL_FLAG & 1 == 0, "guard below rejects symbol block handlers"); // Bail out if the block handler is neither ISEQ nor ifunc - fun.push_insn(block, Insn::GuardAnyBitSet { val: block_handler, mask: Const::CUInt64(0x1), mask_name: None, reason: SideExitReason::BlockParamProxyNotIseqOrIfunc, state: exit_id }); + fun.push_insn(unmodified_block, Insn::GuardAnyBitSet { val: block_handler, mask: Const::CUInt64(0x1), mask_name: None, reason: SideExitReason::BlockParamProxyNotIseqOrIfunc, state: unmodified_exit_id }); // TODO(Shopify/ruby#753): GC root, so we should be able to avoid unnecessary GC tracing - state.stack_push(fun.push_insn(block, Insn::Const { val: Const::Value(unsafe { rb_block_param_proxy }) })); + unmodified_state.stack_push(fun.push_insn(unmodified_block, Insn::Const { val: Const::Value(unsafe { rb_block_param_proxy }) })); + fun.push_insn(unmodified_block, Insn::Jump(BranchEdge { target: join_block, args: unmodified_state.as_args(unmodified_self_param) })); } } + + // Continue compilation from the merged continuation block at the next + // instruction. + queue.push_back((unmodified_state, join_block, insn_idx, local_inval)); + break; } YARVINSN_getblockparam => { - fn new_branch_block( - fun: &mut Function, - insn_idx: u32, - exit_state: &FrameState, - locals_count: usize, - stack_count: usize, - ) -> (BlockId, InsnId, FrameState, InsnId) { - let block = fun.new_block(insn_idx); - let self_param = fun.push_insn(block, Insn::Param); - let mut state = exit_state.clone(); - state.locals.clear(); - state.stack.clear(); - state.locals.extend((0..locals_count).map(|_| fun.push_insn(block, Insn::Param))); - state.stack.extend((0..stack_count).map(|_| fun.push_insn(block, Insn::Param))); - let snapshot = fun.push_insn(block, Insn::Snapshot { state: state.clone() }); - (block, self_param, state, snapshot) - } - fn finish_getblockparam_branch( fun: &mut Function, block: BlockId, @@ -7667,9 +7702,9 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // Set up branch and join blocks. let (modified_block, modified_self_param, mut modified_state, ..) = - new_branch_block(&mut fun, branch_insn_idx, &exit_state, locals_count, stack_count); + fun.new_branch_block(branch_insn_idx, &exit_state, locals_count, stack_count); let (unmodified_block, unmodified_self_param, mut unmodified_state, unmodified_exit_id) = - new_branch_block(&mut fun, branch_insn_idx, &exit_state, locals_count, stack_count); + fun.new_branch_block(branch_insn_idx, &exit_state, locals_count, stack_count); let join_block = insn_idx_to_block.get(&insn_idx).copied().unwrap_or_else(|| fun.new_block(insn_idx)); fun.push_insn(block, Insn::IfTrue { diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 60b0f72da5c8db..68850a71395ab0 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -4735,12 +4735,121 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :block@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v15:CPtr = GetEP 0 - v16:CInt64 = LoadField v15, :_env_data_index_flags@0x1001 - v17:CInt64 = GuardNoBitsSet v16, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v18:CInt64 = LoadField v15, :_env_data_index_specval@0x1002 - v19:CInt64 = GuardAnyBitSet v18, CUInt64(1) - v20:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + v23:CPtr = GetEP 0 + v24:CBool = IsBlockParamModified v23 + IfTrue v24, bb4(v9, v10, v9) + v30:CPtr = GetEP 0 + v31:CInt64 = LoadField v30, :_env_data_index_specval@0x1001 + v32:CInt64 = GuardAnyBitSet v31, CUInt64(1) + v33:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb6(v9, v10, v9, v33) + bb4(v15:BasicObject, v16:BasicObject, v17:BasicObject): + v27:CPtr = GetEP 0 + v28:BasicObject = LoadField v27, :block@0x1010 + Jump bb6(v15, v28, v17, v28) + bb6(v35:BasicObject, v36:BasicObject, v37:BasicObject, v38:BasicObject): + SideExit NoProfileSend recompile + "); + } + + #[test] + fn test_getblockparamproxy_modified() { + eval(" + def test(&block) + b = block + tap(&block) + end + "); + assert_contains_opcode("test", YARVINSN_getblockparamproxy); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :block@0x1000 + v4:NilClass = Const Value(nil) + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :block@1 + v9:NilClass = Const Value(nil) + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): + v17:CPtr = GetEP 0 + v18:CBool = IsBlockParamModified v17 + IfTrue v18, bb4(v11, v12, v13) + v32:BasicObject = GetBlockParam :block, l0, EP@4 + Jump bb6(v11, v32, v13, v32) + bb4(v19:BasicObject, v20:BasicObject, v21:NilClass): + v29:CPtr = GetEP 0 + v30:BasicObject = LoadField v29, :block@0x1001 + Jump bb6(v19, v30, v21, v30) + bb6(v34:BasicObject, v35:BasicObject, v36:NilClass, v37:BasicObject): + v53:CPtr = GetEP 0 + v54:CBool = IsBlockParamModified v53 + IfTrue v54, bb7(v34, v35, v37, v34) + v60:CPtr = GetEP 0 + v61:CInt64 = LoadField v60, :_env_data_index_specval@0x1002 + v62:CInt64 = GuardAnyBitSet v61, CUInt64(1) + v63:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb9(v34, v35, v37, v34, v63) + bb7(v43:BasicObject, v44:BasicObject, v45:BasicObject, v46:BasicObject): + v57:CPtr = GetEP 0 + v58:BasicObject = LoadField v57, :block@0x1001 + Jump bb9(v43, v58, v45, v46, v58) + bb9(v65:BasicObject, v66:BasicObject, v67:BasicObject, v68:BasicObject, v69:BasicObject): + SideExit NoProfileSend recompile + "); + } + + #[test] + fn test_getblockparamproxy_modified_nested_block() { + eval(" + def test(&block) + proc do + b = block + tap(&block) + end + end + "); + assert_snapshot!(hir_string_proc("test"), @" + fn block in test@:4: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:NilClass = Const Value(nil) + Jump bb3(v1, v2) + bb2(): + EntryPoint JIT(0) + v5:BasicObject = LoadArg :self@0 + v6:NilClass = Const Value(nil) + Jump bb3(v5, v6) + bb3(v8:BasicObject, v9:NilClass): + v13:CPtr = GetEP 1 + v14:CBool = IsBlockParamModified v13 + IfTrue v14, bb4(v8, v9) + v26:BasicObject = GetBlockParam :block, l1, EP@3 + Jump bb6(v8, v9, v26) + bb4(v15:BasicObject, v16:NilClass): + v23:CPtr = GetEP 1 + v24:BasicObject = LoadField v23, :block@0x1000 + Jump bb6(v15, v16, v24) + bb6(v28:BasicObject, v29:NilClass, v30:BasicObject): + v44:CPtr = GetEP 1 + v45:CBool = IsBlockParamModified v44 + IfTrue v45, bb7(v28, v30, v28) + v51:CPtr = GetEP 1 + v52:CInt64 = LoadField v51, :_env_data_index_specval@0x1001 + v53:CInt64 = GuardAnyBitSet v52, CUInt64(1) + v54:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb9(v28, v30, v28, v54) + bb7(v36:BasicObject, v37:BasicObject, v38:BasicObject): + v48:CPtr = GetEP 1 + v49:BasicObject = LoadField v48, :block@0x1000 + Jump bb9(v36, v37, v38, v49) + bb9(v56:BasicObject, v57:BasicObject, v58:BasicObject, v59:BasicObject): SideExit NoProfileSend recompile "); } @@ -7909,15 +8018,22 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): v14:ArrayExact = NewArray - v16:CPtr = GetEP 0 - v17:CInt64 = LoadField v16, :_env_data_index_flags@0x1001 - v18:CInt64 = GuardNoBitsSet v17, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v19:CInt64 = LoadField v16, :_env_data_index_specval@0x1002 - v20:CInt64 = GuardAnyBitSet v19, CUInt64(1) - v21:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - v23:BasicObject = Send v14, &block, :map, v21 # SendFallbackReason: Complex argument passing + v24:CPtr = GetEP 0 + v25:CBool = IsBlockParamModified v24 + IfTrue v25, bb4(v9, v10, v14) + v31:CPtr = GetEP 0 + v32:CInt64 = LoadField v31, :_env_data_index_specval@0x1001 + v33:CInt64 = GuardAnyBitSet v32, CUInt64(1) + v34:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb6(v9, v10, v14, v34) + bb4(v16:BasicObject, v17:BasicObject, v18:ArrayExact): + v28:CPtr = GetEP 0 + v29:BasicObject = LoadField v28, :block@0x1010 + Jump bb6(v16, v29, v18, v29) + bb6(v36:BasicObject, v37:BasicObject, v38:ArrayExact, v39:BasicObject): + v42:BasicObject = Send v38, &block, :map, v39 # SendFallbackReason: Complex argument passing CheckInterrupts - Return v23 + Return v42 "); } @@ -7942,15 +8058,22 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): v14:ArrayExact = NewArray - v16:CPtr = GetEP 0 - v17:CInt64 = LoadField v16, :_env_data_index_flags@0x1001 - v18:CInt64 = GuardNoBitsSet v17, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v19:CInt64 = LoadField v16, :_env_data_index_specval@0x1002 - v20:CInt64[0] = GuardBitEquals v19, CInt64(0) - v21:NilClass = Const Value(nil) - v23:BasicObject = Send v14, &block, :map, v21 # SendFallbackReason: Complex argument passing + v24:CPtr = GetEP 0 + v25:CBool = IsBlockParamModified v24 + IfTrue v25, bb4(v9, v10, v14) + v31:CPtr = GetEP 0 + v32:CInt64 = LoadField v31, :_env_data_index_specval@0x1001 + v33:CInt64[0] = GuardBitEquals v32, CInt64(0) + v34:NilClass = Const Value(nil) + Jump bb6(v9, v10, v14, v34) + bb4(v16:BasicObject, v17:BasicObject, v18:ArrayExact): + v28:CPtr = GetEP 0 + v29:BasicObject = LoadField v28, :block@0x1002 + Jump bb6(v16, v29, v18, v29) + bb6(v36:BasicObject, v37:BasicObject, v38:ArrayExact, v39:BasicObject): + v42:BasicObject = Send v38, &block, :map, v39 # SendFallbackReason: Complex argument passing CheckInterrupts - Return v23 + Return v42 "); } @@ -7976,15 +8099,22 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v10:ArrayExact = NewArray - v12:CPtr = GetEP 1 - v13:CInt64 = LoadField v12, :_env_data_index_flags@0x1000 - v14:CInt64 = GuardNoBitsSet v13, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v15:CInt64 = LoadField v12, :_env_data_index_specval@0x1001 - v16:CInt64 = GuardAnyBitSet v15, CUInt64(1) - v17:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - v19:BasicObject = Send v10, &block, :map, v17 # SendFallbackReason: Complex argument passing + v18:CPtr = GetEP 1 + v19:CBool = IsBlockParamModified v18 + IfTrue v19, bb4(v6, v10) + v25:CPtr = GetEP 1 + v26:CInt64 = LoadField v25, :_env_data_index_specval@0x1000 + v27:CInt64 = GuardAnyBitSet v26, CUInt64(1) + v28:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb6(v6, v10, v28) + bb4(v12:BasicObject, v13:ArrayExact): + v22:CPtr = GetEP 1 + v23:BasicObject = LoadField v22, :block@0x1010 + Jump bb6(v12, v13, v23) + bb6(v30:BasicObject, v31:ArrayExact, v32:BasicObject): + v35:BasicObject = Send v31, &block, :map, v32 # SendFallbackReason: Complex argument passing CheckInterrupts - Return v19 + Return v35 "); } @@ -11458,15 +11588,22 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): v14:ArrayExact = NewArray - v16:CPtr = GetEP 0 - v17:CInt64 = LoadField v16, :_env_data_index_flags@0x1001 - v18:CInt64 = GuardNoBitsSet v17, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v19:CInt64 = LoadField v16, :_env_data_index_specval@0x1002 - v20:CInt64 = GuardAnyBitSet v19, CUInt64(1) - v21:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - v23:BasicObject = Send v14, &block, :map, v21 # SendFallbackReason: Complex argument passing + v24:CPtr = GetEP 0 + v25:CBool = IsBlockParamModified v24 + IfTrue v25, bb4(v9, v10, v14) + v31:CPtr = GetEP 0 + v32:CInt64 = LoadField v31, :_env_data_index_specval@0x1001 + v33:CInt64 = GuardAnyBitSet v32, CUInt64(1) + v34:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb6(v9, v10, v14, v34) + bb4(v16:BasicObject, v17:BasicObject, v18:ArrayExact): + v28:CPtr = GetEP 0 + v29:BasicObject = LoadField v28, :block@0x1010 + Jump bb6(v16, v29, v18, v29) + bb6(v36:BasicObject, v37:BasicObject, v38:ArrayExact, v39:BasicObject): + v42:BasicObject = Send v38, &block, :map, v39 # SendFallbackReason: Complex argument passing CheckInterrupts - Return v23 + Return v42 "); } diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 706afea9a896fa..b23c7068fbc5f9 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2466,12 +2466,21 @@ pub(crate) mod hir_build_tests { bb3(v17:BasicObject, v18:BasicObject, v19:BasicObject, v20:BasicObject, v21:BasicObject, v22:NilClass): v29:ArrayExact = ToArray v19 PatchPoint NoEPEscape(test) - v34:CPtr = GetEP 0 - v35:CInt64 = LoadField v34, :_env_data_index_flags@0x1004 - v36:CInt64 = GuardNoBitsSet v35, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v37:CInt64 = LoadField v34, :_env_data_index_specval@0x1005 - v38:CInt64 = GuardAnyBitSet v37, CUInt64(1) - v39:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + v56:CPtr = GetEP 0 + v57:CBool = IsBlockParamModified v56 + IfTrue v57, bb4(v17, v18, v19, v20, v21, v22, v17, v18, v29, v20) + Jump bb5(v17, v18, v19, v20, v21, v22, v17, v18, v29, v20) + bb4(v34:BasicObject, v35:BasicObject, v36:BasicObject, v37:BasicObject, v38:BasicObject, v39:NilClass, v40:BasicObject, v41:BasicObject, v42:ArrayExact, v43:BasicObject): + v60:CPtr = GetEP 0 + v61:BasicObject = LoadField v60, :&@0x1004 + Jump bb6(v34, v35, v36, v37, v61, v39, v40, v41, v42, v43, v61) + bb5(v45:BasicObject, v46:BasicObject, v47:BasicObject, v48:BasicObject, v49:BasicObject, v50:NilClass, v51:BasicObject, v52:BasicObject, v53:ArrayExact, v54:BasicObject): + v63:CPtr = GetEP 0 + v64:CInt64 = LoadField v63, :_env_data_index_specval@0x1005 + v65:CInt64 = GuardAnyBitSet v64, CUInt64(1) + v66:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb6(v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v66) + bb6(v68:BasicObject, v69:BasicObject, v70:BasicObject, v71:BasicObject, v72:BasicObject, v73:NilClass, v74:BasicObject, v75:BasicObject, v76:ArrayExact, v77:BasicObject, v78:BasicObject): SideExit SplatKwNotProfiled "); } @@ -3350,6 +3359,161 @@ pub(crate) mod hir_build_tests { "); } + #[test] + fn test_getblockparamproxy() { + eval(" + def test(&block) = tap(&block) + "); + assert_contains_opcode("test", YARVINSN_getblockparamproxy); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :block@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :block@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + v23:CPtr = GetEP 0 + v24:CBool = IsBlockParamModified v23 + IfTrue v24, bb4(v9, v10, v9) + Jump bb5(v9, v10, v9) + bb4(v15:BasicObject, v16:BasicObject, v17:BasicObject): + v27:CPtr = GetEP 0 + v28:BasicObject = LoadField v27, :block@0x1001 + Jump bb6(v15, v28, v17, v28) + bb5(v19:BasicObject, v20:BasicObject, v21:BasicObject): + v30:CPtr = GetEP 0 + v31:CInt64 = LoadField v30, :_env_data_index_specval@0x1002 + v32:CInt64 = GuardAnyBitSet v31, CUInt64(1) + v33:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb6(v19, v20, v21, v33) + bb6(v35:BasicObject, v36:BasicObject, v37:BasicObject, v38:BasicObject): + v41:BasicObject = Send v37, &block, :tap, v38 # SendFallbackReason: Uncategorized(send) + CheckInterrupts + Return v41 + "); + } + + #[test] + fn test_getblockparamproxy_modified() { + eval(" + def test(&block) + b = block + tap(&block) + end + "); + assert_contains_opcode("test", YARVINSN_getblockparamproxy); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :block@0x1000 + v4:NilClass = Const Value(nil) + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :block@1 + v9:NilClass = Const Value(nil) + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): + v17:CPtr = GetEP 0 + v18:CBool = IsBlockParamModified v17 + IfTrue v18, bb4(v11, v12, v13) + Jump bb5(v11, v12, v13) + bb4(v19:BasicObject, v20:BasicObject, v21:NilClass): + v29:CPtr = GetEP 0 + v30:BasicObject = LoadField v29, :block@0x1001 + Jump bb6(v19, v30, v21, v30) + bb5(v23:BasicObject, v24:BasicObject, v25:NilClass): + v32:BasicObject = GetBlockParam :block, l0, EP@4 + Jump bb6(v23, v32, v25, v32) + bb6(v34:BasicObject, v35:BasicObject, v36:NilClass, v37:BasicObject): + v53:CPtr = GetEP 0 + v54:CBool = IsBlockParamModified v53 + IfTrue v54, bb7(v34, v35, v37, v34) + Jump bb8(v34, v35, v37, v34) + bb7(v43:BasicObject, v44:BasicObject, v45:BasicObject, v46:BasicObject): + v57:CPtr = GetEP 0 + v58:BasicObject = LoadField v57, :block@0x1001 + Jump bb9(v43, v58, v45, v46, v58) + bb8(v48:BasicObject, v49:BasicObject, v50:BasicObject, v51:BasicObject): + v60:CPtr = GetEP 0 + v61:CInt64 = LoadField v60, :_env_data_index_specval@0x1002 + v62:CInt64 = GuardAnyBitSet v61, CUInt64(1) + v63:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb9(v48, v49, v50, v51, v63) + bb9(v65:BasicObject, v66:BasicObject, v67:BasicObject, v68:BasicObject, v69:BasicObject): + v72:BasicObject = Send v68, &block, :tap, v69 # SendFallbackReason: Uncategorized(send) + CheckInterrupts + Return v72 + "); + } + + #[test] + fn test_getblockparamproxy_modified_nested_block() { + eval(" + def test(&block) + proc do + b = block + tap(&block) + end + end + "); + assert_snapshot!(hir_string_proc("test"), @" + fn block in test@:4: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:NilClass = Const Value(nil) + Jump bb3(v1, v2) + bb2(): + EntryPoint JIT(0) + v5:BasicObject = LoadArg :self@0 + v6:NilClass = Const Value(nil) + Jump bb3(v5, v6) + bb3(v8:BasicObject, v9:NilClass): + v13:CPtr = GetEP 1 + v14:CBool = IsBlockParamModified v13 + IfTrue v14, bb4(v8, v9) + Jump bb5(v8, v9) + bb4(v15:BasicObject, v16:NilClass): + v23:CPtr = GetEP 1 + v24:BasicObject = LoadField v23, :block@0x1000 + Jump bb6(v15, v16, v24) + bb5(v18:BasicObject, v19:NilClass): + v26:BasicObject = GetBlockParam :block, l1, EP@3 + Jump bb6(v18, v19, v26) + bb6(v28:BasicObject, v29:NilClass, v30:BasicObject): + v44:CPtr = GetEP 1 + v45:CBool = IsBlockParamModified v44 + IfTrue v45, bb7(v28, v30, v28) + Jump bb8(v28, v30, v28) + bb7(v36:BasicObject, v37:BasicObject, v38:BasicObject): + v48:CPtr = GetEP 1 + v49:BasicObject = LoadField v48, :block@0x1000 + Jump bb9(v36, v37, v38, v49) + bb8(v40:BasicObject, v41:BasicObject, v42:BasicObject): + v51:CPtr = GetEP 1 + v52:CInt64 = LoadField v51, :_env_data_index_specval@0x1001 + v53:CInt64 = GuardAnyBitSet v52, CUInt64(1) + v54:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb9(v40, v41, v42, v54) + bb9(v56:BasicObject, v57:BasicObject, v58:BasicObject, v59:BasicObject): + v62:BasicObject = Send v58, &block, :tap, v59 # SendFallbackReason: Uncategorized(send) + CheckInterrupts + Return v62 + "); + } + #[test] fn test_getblockparam_nested_block() { eval(" @@ -3476,12 +3640,21 @@ pub(crate) mod hir_build_tests { v9:BasicObject = LoadArg :b@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - v19:CPtr = GetEP 0 - v20:CInt64 = LoadField v19, :_env_data_index_flags@0x1002 - v21:CInt64 = GuardNoBitsSet v20, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v22:CInt64 = LoadField v19, :_env_data_index_specval@0x1003 - v23:CInt64 = GuardAnyBitSet v22, CUInt64(1) - v24:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + v31:CPtr = GetEP 0 + v32:CBool = IsBlockParamModified v31 + IfTrue v32, bb4(v11, v12, v13, v11, v12) + Jump bb5(v11, v12, v13, v11, v12) + bb4(v19:BasicObject, v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:BasicObject): + v35:CPtr = GetEP 0 + v36:BasicObject = LoadField v35, :b@0x1002 + Jump bb6(v19, v20, v36, v22, v23, v36) + bb5(v25:BasicObject, v26:BasicObject, v27:BasicObject, v28:BasicObject, v29:BasicObject): + v38:CPtr = GetEP 0 + v39:CInt64 = LoadField v38, :_env_data_index_specval@0x1003 + v40:CInt64 = GuardAnyBitSet v39, CUInt64(1) + v41:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb6(v25, v26, v27, v28, v29, v41) + bb6(v43:BasicObject, v44:BasicObject, v45:BasicObject, v46:BasicObject, v47:BasicObject, v48:BasicObject): SideExit SplatKwNotProfiled "); } @@ -3518,16 +3691,25 @@ pub(crate) mod hir_build_tests { bb3(v17:BasicObject, v18:BasicObject, v19:BasicObject, v20:BasicObject, v21:BasicObject, v22:NilClass): v29:ArrayExact = ToArray v19 PatchPoint NoEPEscape(test) - v34:CPtr = GetEP 0 - v35:CInt64 = LoadField v34, :_env_data_index_flags@0x1004 - v36:CInt64 = GuardNoBitsSet v35, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v37:CInt64 = LoadField v34, :_env_data_index_specval@0x1005 - v38:CInt64[0] = GuardBitEquals v37, CInt64(0) - v39:NilClass = Const Value(nil) - v41:NilClass = GuardType v20, NilClass - v43:BasicObject = Send v17, &block, :foo, v18, v29, v41, v39 # SendFallbackReason: Uncategorized(send) + v56:CPtr = GetEP 0 + v57:CBool = IsBlockParamModified v56 + IfTrue v57, bb4(v17, v18, v19, v20, v21, v22, v17, v18, v29, v20) + Jump bb5(v17, v18, v19, v20, v21, v22, v17, v18, v29, v20) + bb4(v34:BasicObject, v35:BasicObject, v36:BasicObject, v37:BasicObject, v38:BasicObject, v39:NilClass, v40:BasicObject, v41:BasicObject, v42:ArrayExact, v43:BasicObject): + v60:CPtr = GetEP 0 + v61:BasicObject = LoadField v60, :&@0x1004 + Jump bb6(v34, v35, v36, v37, v61, v39, v40, v41, v42, v43, v61) + bb5(v45:BasicObject, v46:BasicObject, v47:BasicObject, v48:BasicObject, v49:BasicObject, v50:NilClass, v51:BasicObject, v52:BasicObject, v53:ArrayExact, v54:BasicObject): + v63:CPtr = GetEP 0 + v64:CInt64 = LoadField v63, :_env_data_index_specval@0x1005 + v65:CInt64[0] = GuardBitEquals v64, CInt64(0) + v66:NilClass = Const Value(nil) + Jump bb6(v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v66) + bb6(v68:BasicObject, v69:BasicObject, v70:BasicObject, v71:BasicObject, v72:BasicObject, v73:NilClass, v74:BasicObject, v75:BasicObject, v76:ArrayExact, v77:BasicObject, v78:BasicObject): + v81:NilClass = GuardType v77, NilClass + v83:BasicObject = Send v74, &block, :foo, v75, v76, v81, v78 # SendFallbackReason: Uncategorized(send) CheckInterrupts - Return v43 + Return v83 "); } @@ -3555,16 +3737,25 @@ pub(crate) mod hir_build_tests { v9:BasicObject = LoadArg :b@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - v19:CPtr = GetEP 0 - v20:CInt64 = LoadField v19, :_env_data_index_flags@0x1002 - v21:CInt64 = GuardNoBitsSet v20, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v22:CInt64 = LoadField v19, :_env_data_index_specval@0x1003 - v23:CInt64 = GuardAnyBitSet v22, CUInt64(1) - v24:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - v26:HashExact = GuardType v12, HashExact - v28:BasicObject = Send v11, &block, :foo, v26, v24 # SendFallbackReason: Uncategorized(send) + v31:CPtr = GetEP 0 + v32:CBool = IsBlockParamModified v31 + IfTrue v32, bb4(v11, v12, v13, v11, v12) + Jump bb5(v11, v12, v13, v11, v12) + bb4(v19:BasicObject, v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:BasicObject): + v35:CPtr = GetEP 0 + v36:BasicObject = LoadField v35, :b@0x1002 + Jump bb6(v19, v20, v36, v22, v23, v36) + bb5(v25:BasicObject, v26:BasicObject, v27:BasicObject, v28:BasicObject, v29:BasicObject): + v38:CPtr = GetEP 0 + v39:CInt64 = LoadField v38, :_env_data_index_specval@0x1003 + v40:CInt64 = GuardAnyBitSet v39, CUInt64(1) + v41:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb6(v25, v26, v27, v28, v29, v41) + bb6(v43:BasicObject, v44:BasicObject, v45:BasicObject, v46:BasicObject, v47:BasicObject, v48:BasicObject): + v51:HashExact = GuardType v47, HashExact + v53:BasicObject = Send v46, &block, :foo, v51, v48 # SendFallbackReason: Uncategorized(send) CheckInterrupts - Return v28 + Return v53 "); } @@ -3592,16 +3783,25 @@ pub(crate) mod hir_build_tests { v9:BasicObject = LoadArg :b@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - v19:CPtr = GetEP 0 - v20:CInt64 = LoadField v19, :_env_data_index_flags@0x1002 - v21:CInt64 = GuardNoBitsSet v20, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v22:CInt64 = LoadField v19, :_env_data_index_specval@0x1003 - v23:CInt64 = GuardAnyBitSet v22, CUInt64(1) - v24:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - v26:HashExact = GuardType v12, HashExact - v28:BasicObject = Send v11, &block, :foo, v26, v24 # SendFallbackReason: Uncategorized(send) + v31:CPtr = GetEP 0 + v32:CBool = IsBlockParamModified v31 + IfTrue v32, bb4(v11, v12, v13, v11, v12) + Jump bb5(v11, v12, v13, v11, v12) + bb4(v19:BasicObject, v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:BasicObject): + v35:CPtr = GetEP 0 + v36:BasicObject = LoadField v35, :b@0x1002 + Jump bb6(v19, v20, v36, v22, v23, v36) + bb5(v25:BasicObject, v26:BasicObject, v27:BasicObject, v28:BasicObject, v29:BasicObject): + v38:CPtr = GetEP 0 + v39:CInt64 = LoadField v38, :_env_data_index_specval@0x1003 + v40:CInt64 = GuardAnyBitSet v39, CUInt64(1) + v41:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb6(v25, v26, v27, v28, v29, v41) + bb6(v43:BasicObject, v44:BasicObject, v45:BasicObject, v46:BasicObject, v47:BasicObject, v48:BasicObject): + v51:HashExact = GuardType v47, HashExact + v53:BasicObject = Send v46, &block, :foo, v51, v48 # SendFallbackReason: Uncategorized(send) CheckInterrupts - Return v28 + Return v53 "); } @@ -3639,12 +3839,21 @@ pub(crate) mod hir_build_tests { bb3(v17:BasicObject, v18:BasicObject, v19:BasicObject, v20:BasicObject, v21:BasicObject, v22:NilClass): v29:ArrayExact = ToArray v19 PatchPoint NoEPEscape(test) - v34:CPtr = GetEP 0 - v35:CInt64 = LoadField v34, :_env_data_index_flags@0x1004 - v36:CInt64 = GuardNoBitsSet v35, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v37:CInt64 = LoadField v34, :_env_data_index_specval@0x1005 - v38:CInt64[0] = GuardBitEquals v37, CInt64(0) - v39:NilClass = Const Value(nil) + v56:CPtr = GetEP 0 + v57:CBool = IsBlockParamModified v56 + IfTrue v57, bb4(v17, v18, v19, v20, v21, v22, v17, v18, v29, v20) + Jump bb5(v17, v18, v19, v20, v21, v22, v17, v18, v29, v20) + bb4(v34:BasicObject, v35:BasicObject, v36:BasicObject, v37:BasicObject, v38:BasicObject, v39:NilClass, v40:BasicObject, v41:BasicObject, v42:ArrayExact, v43:BasicObject): + v60:CPtr = GetEP 0 + v61:BasicObject = LoadField v60, :&@0x1004 + Jump bb6(v34, v35, v36, v37, v61, v39, v40, v41, v42, v43, v61) + bb5(v45:BasicObject, v46:BasicObject, v47:BasicObject, v48:BasicObject, v49:BasicObject, v50:NilClass, v51:BasicObject, v52:BasicObject, v53:ArrayExact, v54:BasicObject): + v63:CPtr = GetEP 0 + v64:CInt64 = LoadField v63, :_env_data_index_specval@0x1005 + v65:CInt64[0] = GuardBitEquals v64, CInt64(0) + v66:NilClass = Const Value(nil) + Jump bb6(v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v66) + bb6(v68:BasicObject, v69:BasicObject, v70:BasicObject, v71:BasicObject, v72:BasicObject, v73:NilClass, v74:BasicObject, v75:BasicObject, v76:ArrayExact, v77:BasicObject, v78:BasicObject): SideExit SplatKwPolymorphic "); } @@ -3675,12 +3884,21 @@ pub(crate) mod hir_build_tests { v9:BasicObject = LoadArg :block@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - v19:CPtr = GetEP 0 - v20:CInt64 = LoadField v19, :_env_data_index_flags@0x1002 - v21:CInt64 = GuardNoBitsSet v20, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v22:CInt64 = LoadField v19, :_env_data_index_specval@0x1003 - v23:CInt64 = GuardAnyBitSet v22, CUInt64(1) - v24:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + v31:CPtr = GetEP 0 + v32:CBool = IsBlockParamModified v31 + IfTrue v32, bb4(v11, v12, v13, v11, v12) + Jump bb5(v11, v12, v13, v11, v12) + bb4(v19:BasicObject, v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:BasicObject): + v35:CPtr = GetEP 0 + v36:BasicObject = LoadField v35, :block@0x1002 + Jump bb6(v19, v20, v36, v22, v23, v36) + bb5(v25:BasicObject, v26:BasicObject, v27:BasicObject, v28:BasicObject, v29:BasicObject): + v38:CPtr = GetEP 0 + v39:CInt64 = LoadField v38, :_env_data_index_specval@0x1003 + v40:CInt64 = GuardAnyBitSet v39, CUInt64(1) + v41:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb6(v25, v26, v27, v28, v29, v41) + bb6(v43:BasicObject, v44:BasicObject, v45:BasicObject, v46:BasicObject, v47:BasicObject, v48:BasicObject): SideExit SplatKwNotNilOrHash "); } @@ -4358,24 +4576,33 @@ pub(crate) mod hir_build_tests { bb3(v18:BasicObject, v19:BasicObject, v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:NilClass): v27:BasicObject = InvokeBuiltin dir_s_open, v18, v19, v20 PatchPoint NoEPEscape(open) - v33:CPtr = GetEP 0 - v34:CInt64 = LoadField v33, :_env_data_index_flags@0x1004 - v35:CInt64 = GuardNoBitsSet v34, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM=CUInt64(512) - v36:CInt64 = LoadField v33, :_env_data_index_specval@0x1005 - v37:CInt64 = GuardAnyBitSet v36, CUInt64(1) - v38:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - CheckInterrupts - v41:CBool[true] = Test v38 - v42 = RefineType v38, Falsy - IfFalse v41, bb4(v18, v19, v20, v21, v22, v27) - v44:ObjectSubclass[BlockParamProxy] = RefineType v38, Truthy - v48:BasicObject = InvokeBlock, v27 # SendFallbackReason: InvokeBlock: not yet specialized - v51:BasicObject = InvokeBuiltin dir_s_close, v18, v27 - CheckInterrupts - Return v48 - bb4(v57, v58, v59, v60, v61, v62): - CheckInterrupts - Return v62 + v47:CPtr = GetEP 0 + v48:CBool = IsBlockParamModified v47 + IfTrue v48, bb5(v18, v19, v20, v21, v22, v27) + Jump bb6(v18, v19, v20, v21, v22, v27) + bb5(v33:BasicObject, v34:BasicObject, v35:BasicObject, v36:BasicObject, v37:BasicObject, v38:BasicObject): + v51:CPtr = GetEP 0 + v52:BasicObject = LoadField v51, :block@0x1004 + Jump bb7(v33, v34, v35, v36, v52, v38, v52) + bb6(v40:BasicObject, v41:BasicObject, v42:BasicObject, v43:BasicObject, v44:BasicObject, v45:BasicObject): + v54:CPtr = GetEP 0 + v55:CInt64 = LoadField v54, :_env_data_index_specval@0x1005 + v56:CInt64 = GuardAnyBitSet v55, CUInt64(1) + v57:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) + Jump bb7(v40, v41, v42, v43, v44, v45, v57) + bb7(v59:BasicObject, v60:BasicObject, v61:BasicObject, v62:BasicObject, v63:BasicObject, v64:BasicObject, v65:BasicObject): + CheckInterrupts + v69:CBool = Test v65 + v70:Falsy = RefineType v65, Falsy + IfFalse v69, bb4(v59, v60, v61, v62, v63, v64) + v72:Truthy = RefineType v65, Truthy + v76:BasicObject = InvokeBlock, v64 # SendFallbackReason: InvokeBlock: not yet specialized + v79:BasicObject = InvokeBuiltin dir_s_close, v59, v64 + CheckInterrupts + Return v76 + bb4(v85:BasicObject, v86:BasicObject, v87:BasicObject, v88:BasicObject, v89:BasicObject, v90:BasicObject): + CheckInterrupts + Return v90 "); } diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 537813fee7438d..db6aef5d8cc31c 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -226,7 +226,6 @@ make_counters! { exit_obj_to_string_fallback, exit_interrupt, exit_stackoverflow, - exit_block_param_proxy_modified, exit_block_param_proxy_not_iseq_or_ifunc, exit_block_param_proxy_not_nil, exit_block_param_wb_required, @@ -608,7 +607,6 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { ObjToStringFallback => exit_obj_to_string_fallback, Interrupt => exit_interrupt, StackOverflow => exit_stackoverflow, - BlockParamProxyModified => exit_block_param_proxy_modified, BlockParamProxyNotIseqOrIfunc => exit_block_param_proxy_not_iseq_or_ifunc, BlockParamProxyNotNil => exit_block_param_proxy_not_nil, BlockParamWbRequired => exit_block_param_wb_required, From d368d42ec6b50cece947556a2185ce73fa493bfe Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 30 Mar 2026 13:09:23 -0700 Subject: [PATCH 3/7] ZJIT: Skip TestOpenURIProxy on Linux (#16606) Same random SystemStackError as TestOpenURI and TestOpenURISSL. --- test/.excludes-zjit/TestOpenURIProxy.rb | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/.excludes-zjit/TestOpenURIProxy.rb diff --git a/test/.excludes-zjit/TestOpenURIProxy.rb b/test/.excludes-zjit/TestOpenURIProxy.rb new file mode 100644 index 00000000000000..f08a2d7e89b201 --- /dev/null +++ b/test/.excludes-zjit/TestOpenURIProxy.rb @@ -0,0 +1,3 @@ +if RUBY_PLATFORM =~ /linux/ + exclude(/test_/, 'randomly fails with SystemStackError (Shopify/ruby#964)') +end From 98e55e7b501bb34819806522e6dce357030ea315 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 30 Mar 2026 13:33:27 -0700 Subject: [PATCH 4/7] ZJIT: Add --zjit-max-versions option (#16607) Allow configuring the maximum number of compiled versions per ISEQ via --zjit-max-versions=num (default: 2). Like YJIT's equivalent, this option is hidden from --help output. --- zjit/src/codegen.rs | 17 ++++++++++++----- zjit/src/codegen_tests.rs | 7 ++++--- zjit/src/hir.rs | 2 +- zjit/src/options.rs | 8 ++++++++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index b2b1db1cf010d9..d93ec482c6f95a 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -26,8 +26,15 @@ use crate::hir_type::{types, Type}; use crate::options::{get_option, PerfMap}; use crate::cast::IntoUsize; -/// At the moment, we support recompiling each ISEQ only once. -pub const MAX_ISEQ_VERSIONS: usize = 2; +/// Default maximum number of compiled versions per ISEQ. +const DEFAULT_MAX_VERSIONS: usize = 2; + +/// Maximum number of compiled versions per ISEQ. +/// Configurable via --zjit-max-versions (default: 2). +pub fn max_iseq_versions() -> usize { + unsafe { crate::options::OPTIONS.as_ref() } + .map_or(DEFAULT_MAX_VERSIONS, |opts| opts.max_versions) +} /// Sentinel program counter stored in C frames when runtime checks are enabled. const PC_POISON: Option<*const VALUE> = if cfg!(feature = "runtime_checks") { @@ -216,7 +223,7 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool) pub fn invalidate_iseq_version(cb: &mut CodeBlock, iseq: IseqPtr, version: &mut IseqVersionRef) { let payload = get_or_create_iseq_payload(iseq); if unsafe { version.as_ref() }.status != IseqStatus::Invalidated - && payload.versions.len() < MAX_ISEQ_VERSIONS + && payload.versions.len() < max_iseq_versions() { unsafe { version.as_mut() }.status = IseqStatus::Invalidated; unsafe { rb_iseq_reset_jit_func(iseq) }; @@ -300,8 +307,8 @@ fn gen_iseq(cb: &mut CodeBlock, iseq: IseqPtr, function: Option<&Function>) -> R Some(IseqStatus::CantCompile(err)) => return Err(err.clone()), _ => {}, } - // If the ISEQ already hax MAX_ISEQ_VERSIONS, do not compile a new version. - if payload.versions.len() == MAX_ISEQ_VERSIONS { + // If the ISEQ already has max versions, do not compile a new version. + if payload.versions.len() >= max_iseq_versions() { return Err(CompileError::IseqVersionLimitReached); } diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index ee4966d19fe68b..4dbc42a5b41192 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -3,7 +3,7 @@ use super::{gen_insn, JITState}; use crate::asm::CodeBlock; use crate::backend::lir::Assembler; -use crate::codegen::MAX_ISEQ_VERSIONS; +use crate::codegen::max_iseq_versions; use crate::cruby::*; use crate::hir::{Insn, iseq_to_hir}; use crate::options::{rb_zjit_prepare_options, set_call_threshold}; @@ -4937,13 +4937,14 @@ fn test_invokesuper_with_local_written_by_blockiseq() { #[test] fn test_max_iseq_versions() { + let max_versions = max_iseq_versions(); eval(&format!(" TEST = -1 def test = TEST # compile and invalidate MAX+1 times i = 0 - while i < {MAX_ISEQ_VERSIONS} + 1 + while i < {max_versions} + 1 test; test # compile a version Object.send(:remove_const, :TEST) @@ -4956,7 +4957,7 @@ fn test_max_iseq_versions() { // It should not exceed MAX_ISEQ_VERSIONS let iseq = get_method_iseq("self", "test"); let payload = get_or_create_iseq_payload(iseq); - assert_eq!(payload.versions.len(), MAX_ISEQ_VERSIONS); + assert_eq!(payload.versions.len(), max_iseq_versions()); // The last call should not discard the JIT code assert!(matches!(unsafe { payload.versions.last().unwrap().as_ref() }.status, IseqStatus::Compiled(_))); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 9c1f0d84a1db1d..ce00f0c0b8601c 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5129,7 +5129,7 @@ impl Function { // SideExits would just add overhead (the exit fires every time without benefit). // Keep them as Send fallbacks so the interpreter handles them directly. let payload = get_or_create_iseq_payload(self.iseq); - if payload.versions.len() + 1 >= crate::codegen::MAX_ISEQ_VERSIONS { + if payload.versions.len() + 1 >= crate::codegen::max_iseq_versions() { return; } for block in self.rpo() { diff --git a/zjit/src/options.rs b/zjit/src/options.rs index 99ccbcaf8b851c..fdf4a14e66fd38 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -105,6 +105,9 @@ pub struct Options { /// Path to a file where compiled ISEQs will be saved. pub log_compiled_iseqs: Option, + + /// Maximum number of versions per ISEQ + pub max_versions: usize, } impl Default for Options { @@ -130,6 +133,7 @@ impl Default for Options { perf: None, allowed_iseqs: None, log_compiled_iseqs: None, + max_versions: 2, } } } @@ -340,6 +344,10 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> { Err(_) => return None, }, + ("max-versions", _) => match opt_val.parse() { + Ok(n) => options.max_versions = n, + Err(_) => return None, + }, ("stats-quiet", _) => { options.stats = true; From 72f309fe529ce5c4778b06201c2ba044c505a2e7 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Mon, 30 Mar 2026 18:10:12 -0500 Subject: [PATCH 5/7] [DOC] Doc for Pathname class (#16352) --- lib/pathname.rb | 1 - pathname_builtin.rb | 128 +++++++++++++++++++++++++------------------- 2 files changed, 74 insertions(+), 55 deletions(-) diff --git a/lib/pathname.rb b/lib/pathname.rb index 78c440416ea060..5474fb6358c4a0 100644 --- a/lib/pathname.rb +++ b/lib/pathname.rb @@ -9,7 +9,6 @@ # # For documentation, see class Pathname. # - class Pathname # * Find * # # Iterates over the directory tree in a depth first manner, yielding a diff --git a/pathname_builtin.rb b/pathname_builtin.rb index cce1cc84a15054..ff13d68f8e8407 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1,76 +1,96 @@ # frozen_string_literal: true # -# = pathname.rb +# A \Pathname object contains a string directory path or filepath; +# it does not represent a corresponding actual file or directory +# -- which in fact may or may not exist. # -# Object-Oriented Pathname Class +# A \Pathname object is immutable (except for method #freeze). # -# Author:: Tanaka Akira -# Documentation:: Author and Gavin Sinclair +# A pathname may be relative or absolute: # -# For documentation, see class Pathname. +# Pathname.new('lib') # => # +# Pathname.new('/usr/local/bin') # => # # - +# == Convenience Methods +# +# The class provides *all* functionality from class File and module FileTest, +# along with some functionality from class Dir and module FileUtils. +# +# Here's an example string path and corresponding \Pathname object: +# +# path = 'lib/fileutils.rb' +# pn = Pathname.new(path) # => # +# +# Each of these method pairs (\Pathname vs. \File) gives exactly the same result: +# +# pn.size # => 83777 +# File.size(path) # => 83777 +# +# pn.directory? # => false +# File.directory?(path) # => false +# +# pn.read.size # => 81074 +# File.read(path).size# # => 81074 +# +# Each of these method pairs gives similar results, +# but each \Pathname method returns a more versatile \Pathname object, +# instead of a string: +# +# pn.dirname # => # +# File.dirname(path) # => "lib" # -# Pathname represents the name of a file or directory on the filesystem, -# but not the file itself. +# pn.basename # => # +# File.basename(path) # => "fileutils.rb" # -# The pathname depends on the Operating System: Unix, Windows, etc. -# This library works with pathnames of local OS, however non-Unix pathnames -# are supported experimentally. +# pn.split # => [#, #] +# File.split(path) # => ["lib", "fileutils.rb"] # -# A Pathname can be relative or absolute. It's not until you try to -# reference the file that it even matters whether the file exists or not. +# Each of these methods takes a block: # -# Pathname is immutable. It has no method for destructive update. +# pn.open do |file| +# p file +# end +# File.open(path) do |file| +# p file +# end # -# The goal of this class is to manipulate file path information in a neater -# way than standard Ruby provides. The examples below demonstrate the -# difference. +# The outputs for each: # -# *All* functionality from File, FileTest, and some from Dir and FileUtils is -# included, in an unsurprising way. It is essentially a facade for all of -# these, and more. +# # +# # # -# == Examples +# Each of these methods takes a block: # -# === Example 1: Using Pathname +# pn.each_line do |line| +# p line +# break +# end +# File.foreach(path) do |line| +# p line +# break +# end # -# require 'pathname' -# pn = Pathname.new("/usr/bin/ruby") -# size = pn.size # 27662 -# isdir = pn.directory? # false -# dir = pn.dirname # Pathname:/usr/bin -# base = pn.basename # Pathname:ruby -# dir, base = pn.split # [Pathname:/usr/bin, Pathname:ruby] -# data = pn.read -# pn.open { |f| _ } -# pn.each_line { |line| _ } +# The outputs for each: # -# === Example 2: Using standard Ruby +# "# frozen_string_literal: true\n" +# "# frozen_string_literal: true\n" # -# pn = "/usr/bin/ruby" -# size = File.size(pn) # 27662 -# isdir = File.directory?(pn) # false -# dir = File.dirname(pn) # "/usr/bin" -# base = File.basename(pn) # "ruby" -# dir, base = File.split(pn) # ["/usr/bin", "ruby"] -# data = File.read(pn) -# File.open(pn) { |f| _ } -# File.foreach(pn) { |line| _ } +# == More Methods # -# === Example 3: Special features +# Here is a sampling of other available methods: # -# p1 = Pathname.new("/usr/lib") # Pathname:/usr/lib -# p2 = p1 + "ruby/1.8" # Pathname:/usr/lib/ruby/1.8 -# p3 = p1.parent # Pathname:/usr -# p4 = p2.relative_path_from(p3) # Pathname:lib/ruby/1.8 -# pwd = Pathname.pwd # Pathname:/home/gavin -# pwd.absolute? # true -# p5 = Pathname.new "." # Pathname:. -# p5 = p5 + "music/../articles" # Pathname:music/../articles -# p5.cleanpath # Pathname:articles -# p5.realpath # Pathname:/home/gavin/articles -# p5.children # [Pathname:/home/gavin/articles/linux, ...] +# p1 = Pathname.new('/usr/lib') # => # +# p1.absolute? # => true +# p2 = p1 + 'ruby/4.0' # => # +# p3 = p1.parent # => # +# p4 = p2.relative_path_from(p3) # => # +# p4.absolute? # => false +# p5 = Pathname.new('.') # => # +# p6 = p5 + 'usr/../var' # => # +# p6.cleanpath # => # +# p6.realpath # => # +# p6.children.take(2) +# # => [#, #] # # == Breakdown of functionality # From 34c94b31ab0be79792a48e42e3a6611f300a0dc2 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Mon, 30 Mar 2026 19:24:20 -0400 Subject: [PATCH 6/7] [DOC] Fix formatting in IO Colon should be outside of the code. --- io.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io.c b/io.c index ab04d8df22864c..d74aa7d5c36ec4 100644 --- a/io.c +++ b/io.c @@ -15113,7 +15113,7 @@ set_LAST_READ_LINE(VALUE val, ID _x, VALUE *_y) * - +:binmode+: If a truthy value, specifies the mode as binary, text-only otherwise. * - +:autoclose+: If a truthy value, specifies that the +fd+ will close * when the stream closes; otherwise it remains open. - * - +:path:+ If a string value is provided, it is used in #inspect and is available as + * - +:path+: If a string value is provided, it is used in #inspect and is available as * #path method. * * Also available are the options offered in String#encode, From f6adf1c48f3678454a682b024bcf99ebfa3caa69 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Mon, 30 Mar 2026 14:55:31 -0400 Subject: [PATCH 7/7] ZJIT: Use a slice for ISeq `opt_table` for quick one-off access This gets rid of the copying of the entire opt_table in function_stub_hit() since it only reads one element. HIR builder still does a copy and that's unchanged. It needs to read the whole table. --- zjit/src/codegen.rs | 12 ++++++++---- zjit/src/cruby.rs | 18 ++++++++++++++++++ zjit/src/hir.rs | 21 +-------------------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index d93ec482c6f95a..aadb2da8cff9c4 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -3076,11 +3076,15 @@ c_callable! { unsafe { Rc::increment_strong_count(iseq_call_ptr as *const IseqCall); } let iseq_call = unsafe { Rc::from_raw(iseq_call_ptr as *const IseqCall) }; let iseq = iseq_call.iseq.get(); - let entry_insn_idxs = crate::hir::jit_entry_insns(iseq); + let params = unsafe { iseq.params() }; + let entry_idx = iseq_call.jit_entry_idx.to_usize(); + let entry_insn_idx = params.opt_table_slice().get(entry_idx) + .unwrap_or_else(|| panic!("function_stub: opt_table out of bounds. {params:#?}, entry_idx={entry_idx}")) + .as_u32(); // gen_push_frame() doesn't set PC or ISEQ, so we need to set them before exit. // function_stub_hit_body() may allocate and call gc_validate_pc(), so we always set PC and ISEQ. // Clear jit_return so the interpreter reads cfp->pc and cfp->iseq directly. - let pc = unsafe { rb_iseq_pc_at_idx(iseq, entry_insn_idxs[iseq_call.jit_entry_idx.to_usize()]) }; + let pc = unsafe { rb_iseq_pc_at_idx(iseq, entry_insn_idx) }; unsafe { rb_set_cfp_pc(cfp, pc) }; unsafe { (*cfp)._iseq = iseq }; unsafe { (*cfp).jit_return = std::ptr::null_mut() }; @@ -3501,7 +3505,7 @@ impl Assembler { /// Store info about a JIT entry point pub struct JITEntry { - /// Index that corresponds to [crate::hir::jit_entry_insns] + /// Index that corresponds to an entry in [crate::cruby::IseqParameters::opt_table_slice] jit_entry_idx: usize, /// Position where the entry point starts start_addr: Cell>, @@ -3524,7 +3528,7 @@ pub struct IseqCall { /// Callee ISEQ that start_addr jumps to pub iseq: Cell, - /// Index that corresponds to [crate::hir::jit_entry_insns] + /// Index that corresponds to an entry in [crate::cruby::IseqParameters::opt_table_slice] jit_entry_idx: u16, /// Argument count passing to the HIR function diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 93079a6b5492da..ee9f7fa7e175ef 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -738,6 +738,24 @@ impl IseqAccess for IseqPtr { } } +impl IseqParameters { + /// The `opt_table` is a mapping where `opt_table[number_of_optional_parameters_filled]` + /// gives the YARV entry point of ISeq as an index of the iseq_encoded array. + /// This method gives over the table that additionally works when `opt_num==0`, + /// when the table is stored as `NULL` and implicit. + /// The table stores the indexes as raw VALUE integers; they are not tagged as fixnum. + pub fn opt_table_slice(&self) -> &[VALUE] { + let opt_num: usize = self.opt_num.try_into().expect("ISeq opt_num should always >=0"); + if opt_num > 0 { + // The table has size=opt_num+1 because opt_table[opt_num] is valid (all optionals filled) + unsafe { std::slice::from_raw_parts(self.opt_table, opt_num + 1) } + } else { + // The ISeq entry point is index 0 when there are no optional parameters + &[VALUE(0)] + } + } +} + impl From for VALUE { /// For `.into()` convenience fn from(iseq: IseqPtr) -> Self { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index ce00f0c0b8601c..4e38b261308110 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -6743,25 +6743,6 @@ fn insn_idx_at_offset(idx: u32, offset: i64) -> u32 { ((idx as isize) + (offset as isize)) as u32 } -/// List of insn_idx that starts a JIT entry block -pub fn jit_entry_insns(iseq: IseqPtr) -> Vec { - // TODO(alan): Make an iterator type for this instead of copying all of the opt_table each call - let params = unsafe { iseq.params() }; - let opt_num = params.opt_num; - if opt_num > 0 { - let mut result = vec![]; - - let opt_table = params.opt_table; // `opt_num + 1` entries - for opt_idx in 0..=opt_num as isize { - let insn_idx = unsafe { opt_table.offset(opt_idx).read().as_u32() }; - result.push(insn_idx); - } - result - } else { - vec![0] - } -} - struct BytecodeInfo { jump_targets: Vec, } @@ -6921,7 +6902,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { fun.was_invalidated_for_singleton_class_creation = payload.was_invalidated_for_singleton_class_creation; // Compute a map of PC->Block by finding jump targets - let jit_entry_insns = jit_entry_insns(iseq); + let jit_entry_insns = unsafe { iseq.params() }.opt_table_slice().iter().copied().map(VALUE::as_u32).collect::>(); let BytecodeInfo { jump_targets } = compute_bytecode_info(iseq, &jit_entry_insns); // Make all empty basic blocks. The ordering of the BBs matters for getting fallthrough jumps