From 7ea07dd74d2e3ca41ddb86ebc108879b2c42b7a4 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:20:45 +0200 Subject: [PATCH 1/9] [Bug #21994] Drop warning for ambiguous regexp use https://bugs.ruby-lang.org/issues/21994 --- parse.y | 8 +------- prism/prism.c | 5 ++++- test/prism/result/warnings_test.rb | 7 ++++--- test/ripper/test_parser_events.rb | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/parse.y b/parse.y index 39018e3621c367..55403f7bde96fa 100644 --- a/parse.y +++ b/parse.y @@ -9174,12 +9174,7 @@ static int arg_ambiguous(struct parser_params *p, char c) { #ifndef RIPPER - if (c == '/') { - rb_warning1("ambiguity between regexp and two divisions: wrap regexp in parentheses or add a space after '%c' operator", WARN_I(c)); - } - else { - rb_warning1("ambiguous first argument; put parentheses or a space even after '%c' operator", WARN_I(c)); - } + rb_warning1("ambiguous first argument; put parentheses or a space even after '%c' operator", WARN_I(c)); #else dispatch1(arg_ambiguous, rb_usascii_str_new(&c, 1)); #endif @@ -11019,7 +11014,6 @@ parser_yylex(struct parser_params *p) } pushback(p, c); if (IS_SPCARG(c)) { - arg_ambiguous(p, '/'); p->lex.strterm = NEW_STRTERM(str_regexp, '/', 0); return tREGEXP_BEG; } diff --git a/prism/prism.c b/prism/prism.c index 57860692cc3f41..a7e661ca169eba 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -11043,7 +11043,10 @@ parser_lex(pm_parser_t *parser) { } if (lex_state_spcarg_p(parser, space_seen)) { - pm_parser_warn_token(parser, &parser->current, PM_WARN_AMBIGUOUS_SLASH); + // https://bugs.ruby-lang.org/issues/21994 + if (parser->version <= PM_OPTIONS_VERSION_CRUBY_4_0) { + pm_parser_warn_token(parser, &parser->current, PM_WARN_AMBIGUOUS_SLASH); + } lex_mode_push_regexp(parser, '\0', '/'); LEX(PM_TOKEN_REGEXP_BEGIN); } diff --git a/test/prism/result/warnings_test.rb b/test/prism/result/warnings_test.rb index 27f1119b98333c..0818a554e40fff 100644 --- a/test/prism/result/warnings_test.rb +++ b/test/prism/result/warnings_test.rb @@ -19,7 +19,8 @@ def test_ambiguous_ustar end def test_ambiguous_regexp - assert_warning("a /b/", "wrap regexp in parentheses") + assert_warning("a /b/", "wrap regexp in parentheses", compare: false, version: "4.0") + refute_warning("a /b/", compare: false, version: "4.1") end def test_ambiguous_ampersand @@ -408,8 +409,8 @@ def test_warnings_verbosity assert_equal "END in method; use at_exit", warning.message assert_equal :default, warning.level - warning = Prism.parse("foo /regexp/").warnings.first - assert_equal "ambiguous `/`; wrap regexp in parentheses or add a space after `/` operator", warning.message + warning = Prism.parse("foo +1").warnings.first + assert_equal "ambiguous first argument; put parentheses or a space even after `+` operator", warning.message assert_equal :verbose, warning.level end diff --git a/test/ripper/test_parser_events.rb b/test/ripper/test_parser_events.rb index 3e72c7a331530a..8b835d3377b50f 100644 --- a/test/ripper/test_parser_events.rb +++ b/test/ripper/test_parser_events.rb @@ -233,7 +233,7 @@ def test_aref_field def test_arg_ambiguous thru_arg_ambiguous = false - parse('m //', :on_arg_ambiguous) {thru_arg_ambiguous = true} + parse('m +1', :on_arg_ambiguous) {thru_arg_ambiguous = true} assert_equal true, thru_arg_ambiguous end From 4f5dc8606ffd238aeaff5ef99000d652b35a2127 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Thu, 2 Jul 2026 15:31:48 -0400 Subject: [PATCH 2/9] YJIT: implement better getblockparamproxy dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` Results — YJIT enabled: block.call vs yield, before/after ┌──────────────────────┬─────────────────────┬────────────────────┬────────────┬ │ operation │ before (2a23415062) │ after (dd4711e727) │ speedup │ ├──────────────────────┼─────────────────────┼────────────────────┼────────────┼ │ yield (control) │ 58.57M i/s │ 58.56M i/s │ — (stable) │ ├──────────────────────┼─────────────────────┼────────────────────┼────────────┼ │ call (0 args) │ 32.95M i/s │ 57.37M i/s │ 1.74× │ ├──────────────────────┼─────────────────────┼────────────────────┼────────────┼ │ yield(1,2) (control) │ 47.25M i/s │ 51.64M i/s │ — │ ├──────────────────────┼─────────────────────┼────────────────────┼────────────┼ │ call(1,2) (2 args) │ 31.25M i/s │ 51.62M i/s │ 1.65× │ └──────────────────────┴─────────────────────┴────────────────────┴────────────┴ ``` --- yjit/src/codegen.rs | 114 ++++++++++++++++++++++++++++++++++++++++---- yjit/src/stats.rs | 6 ++- 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index fcddb18ac7e5c9..1cd4c6c70df8a3 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -7486,7 +7486,7 @@ fn gen_send_bmethod( } let frame_type = VM_FRAME_MAGIC_BLOCK | VM_FRAME_FLAG_BMETHOD | VM_FRAME_FLAG_LAMBDA; - perf_call! { gen_send_iseq(jit, asm, iseq, ci, frame_type, Some(capture.ep), cme, block, flags, argc, None) } + perf_call! { gen_send_iseq(jit, asm, iseq, ci, frame_type, Some(capture.ep), cme, block, flags, argc, None, false) } } /// The kind of a value an ISEQ returns @@ -7582,6 +7582,10 @@ fn gen_send_iseq( flags: u32, argc: i32, captured_opnd: Option, + // The block param proxy receiver is on the stack below the args. self/EP come from + // the captured block (as for invokeblock); the proxy is shifted off after the + // overflow check so the rest of the frame setup is a plain invokeblock. + proxy_recv: bool, ) -> Option { // Argument count. We will change this as we gather values from // sources to satisfy the callee's parameters. To help make sense @@ -7747,6 +7751,13 @@ fn gen_send_iseq( && !get_iseq_flags_ambiguous_param0(iseq) }; if block_arg0_splat { + // The block param proxy removes its receiver from the stack only after the + // overflow check (see proxy_recv below). arg0 auto-splat needs a side exit past + // that point, which can't reconstruct the proxy at the opt_send_without_block PC. + if proxy_recv { + gen_counter_incr(jit, asm, Counter::send_bpp_arg0_splat); + return None; + } // If block_arg0_splat, we still need side exits after splat, but // the splat modifies the stack which breaks side exits. So bail out. if splat_call { @@ -7876,7 +7887,7 @@ fn gen_send_iseq( } IseqReturn::Value(value) => { // Pop receiver and arguments - asm.stack_pop(argc as usize + if captured_opnd.is_some() { 0 } else { 1 }); + asm.stack_pop(argc as usize + if captured_opnd.is_some() && !proxy_recv { 0 } else { 1 }); // Push the return value let stack_ret = asm.stack_push(Type::from(value)); @@ -7903,6 +7914,11 @@ fn gen_send_iseq( asm.cmp(CFP, stack_limit); asm.jbe(Target::side_exit(Counter::guard_send_se_cf_overflow)); + // Remove the block param proxy receiver from the stack, mirroring vm_invoke_block_opt_call + if proxy_recv { + handle_opt_send_shift_stack(asm, argc); + } + if iseq_has_rest && splat_call { // Insert length guard for a call to copy_splat_args_for_rest_callee() // that will come later. We will have made changes to @@ -9155,9 +9171,11 @@ fn gen_send_general( // Don't compile calls through singleton classes to avoid retaining the receiver. // Make an exception for class methods since classes tend to be retained anyways. - // Also compile calls on top_self to help tests. + // Also compile calls on top_self to help tests. The block param proxy is an + // immortal global root, so retaining it is a non-issue. if VALUE(0) != unsafe { FL_TEST(comptime_recv_klass, VALUE(RUBY_FL_SINGLETON as usize)) } && comptime_recv != unsafe { rb_vm_top_self() } + && comptime_recv != unsafe { rb_block_param_proxy } && !unsafe { RB_TYPE_P(comptime_recv, RUBY_T_CLASS) } && !unsafe { RB_TYPE_P(comptime_recv, RUBY_T_MODULE) } { gen_counter_incr(jit, asm, Counter::send_singleton_class); @@ -9244,7 +9262,7 @@ fn gen_send_general( VM_METHOD_TYPE_ISEQ => { let iseq = unsafe { get_def_iseq_ptr((*cme).def) }; let frame_type = VM_FRAME_MAGIC_METHOD | VM_ENV_FLAG_LOCAL; - return perf_call! { gen_send_iseq(jit, asm, iseq, ci, frame_type, None, cme, block, flags, argc, None) }; + return perf_call! { gen_send_iseq(jit, asm, iseq, ci, frame_type, None, cme, block, flags, argc, None, false) }; } VM_METHOD_TYPE_CFUNC => { return perf_call! { gen_send_cfunc( @@ -9489,8 +9507,7 @@ fn gen_send_general( return jump_to_next_insn(jit, asm); } OPTIMIZED_METHOD_TYPE_BLOCK_CALL => { - gen_counter_incr(jit, asm, Counter::send_optimized_method_block_call); - return None; + return gen_send_block_param_proxy(jit, asm, ci, block, flags, argc); } OPTIMIZED_METHOD_TYPE_STRUCT_AREF => { if flags & VM_CALL_ARGS_SPLAT != 0 { @@ -9770,7 +9787,7 @@ fn gen_invokeblock_specialized( Counter::guard_invokeblock_iseq_block_changed, ); - perf_call! { gen_send_iseq(jit, asm, comptime_iseq, ci, VM_FRAME_MAGIC_BLOCK, None, 0 as _, None, flags, argc, Some(captured_opnd)) } + perf_call! { gen_send_iseq(jit, asm, comptime_iseq, ci, VM_FRAME_MAGIC_BLOCK, None, 0 as _, None, flags, argc, Some(captured_opnd), false) } } else if comptime_handler.0 & 0x3 == 0x3 { // VM_BH_IFUNC_P // We aren't handling CALLER_SETUP_ARG and CALLER_REMOVE_EMPTY_KW_SPLAT yet. if flags & VM_CALL_ARGS_SPLAT != 0 { @@ -9831,6 +9848,87 @@ fn gen_invokeblock_specialized( } } +// blk.call where blk is the block param proxy. Inline the block invocation the way invokeblock does +// instead of dispatching through the proxy's singleton method. The receiver's singleton class was already +// guarded by the caller, which is enough to prove the receiver is the (unique) block param proxy. +fn gen_send_block_param_proxy( + jit: &mut JITState, + asm: &mut Assembler, + ci: *const rb_callinfo, + block: Option, + flags: u32, + argc: i32, +) -> Option { + // Anything fancier than a plain call falls back to dynamic dispatch, which + // materializes a Proc and dispatches normally (also handling redefinition). + if block.is_some() { + gen_counter_incr(jit, asm, Counter::send_bpp_not_simple); + return None; + } + if flags & (VM_CALL_ARGS_SPLAT | VM_CALL_KWARG | VM_CALL_KW_SPLAT | VM_CALL_ARGS_BLOCKARG | VM_CALL_OPT_SEND) != 0 { + gen_counter_incr(jit, asm, Counter::send_bpp_not_simple); + return None; + } + + // Fall back to dynamic dispatch if this callsite is megamorphic + if asm.ctx.get_chain_depth() >= SEND_MAX_DEPTH { + gen_counter_incr(jit, asm, Counter::send_bpp_megamorphic); + return None; + } + + // The block handler lives in the local EP: the same source both the interpreter's + // vm_call_opt_block_call and the getblockparamproxy instruction read from. + let cfp = jit.get_cfp(); + let lep = unsafe { rb_vm_ep_local_ep(get_cfp_ep(cfp)) }; + let comptime_handler = unsafe { *lep.offset(VM_ENV_DATA_INDEX_SPECVAL as isize) }; + + // Only specialize an ISEQ block; other block handler types fall back to dynamic dispatch. + if comptime_handler.0 & 0x3 != 0x1 { // VM_BH_ISEQ_BLOCK_P + gen_counter_incr(jit, asm, Counter::send_bpp_not_iseq_block); + return None; + } + + if !assume_bop_not_redefined(jit, asm, PROC_REDEFINED_OP_FLAG, BOP_CALL) { + return None; + } + + gen_counter_incr(jit, asm, Counter::send_bpp_dispatch); + + asm_comment!(asm, "get local EP"); + let ep_opnd = gen_get_lep(jit, asm); + let block_handler_opnd = asm.load( + Opnd::mem(64, ep_opnd, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL) + ); + + asm_comment!(asm, "guard block_handler type"); + let tag_opnd = asm.and(block_handler_opnd, 0x3.into()); + asm.cmp(tag_opnd, 0x1.into()); // VM_BH_ISEQ_BLOCK_P + jit_chain_guard( + JCC_JNE, + jit, + asm, + SEND_MAX_DEPTH, + Counter::guard_invokeblock_tag_changed, + ); + + let comptime_captured = unsafe { ((comptime_handler.0 & !0x3) as *const rb_captured_block).as_ref().unwrap() }; + let comptime_iseq = unsafe { *comptime_captured.code.iseq.as_ref() }; + + asm_comment!(asm, "guard known ISEQ"); + let captured_opnd = asm.and(block_handler_opnd, Opnd::Imm(!0x3)); + let iseq_opnd = asm.load(Opnd::mem(64, captured_opnd, SIZEOF_VALUE_I32 * 2)); + asm.cmp(iseq_opnd, VALUE::from(comptime_iseq).into()); + jit_chain_guard( + JCC_JNE, + jit, + asm, + SEND_MAX_DEPTH, + Counter::guard_invokeblock_iseq_block_changed, + ); + + perf_call! { gen_send_iseq(jit, asm, comptime_iseq, ci, VM_FRAME_MAGIC_BLOCK, None, 0 as _, None, flags, argc, Some(captured_opnd), true) } +} + fn gen_invokesuper( jit: &mut JITState, asm: &mut Assembler, @@ -10000,7 +10098,7 @@ fn gen_invokesuper_specialized( VM_METHOD_TYPE_ISEQ => { let iseq = unsafe { get_def_iseq_ptr((*cme).def) }; let frame_type = VM_FRAME_MAGIC_METHOD | VM_ENV_FLAG_LOCAL; - perf_call! { gen_send_iseq(jit, asm, iseq, ci, frame_type, None, cme, Some(block), ci_flags, argc, None) } + perf_call! { gen_send_iseq(jit, asm, iseq, ci, frame_type, None, cme, Some(block), ci_flags, argc, None, false) } } VM_METHOD_TYPE_CFUNC => { perf_call! { gen_send_cfunc(jit, asm, ci, cme, Some(block), None, ci_flags, argc) } diff --git a/yjit/src/stats.rs b/yjit/src/stats.rs index b23cd916184f44..82381b4a5a4c1b 100644 --- a/yjit/src/stats.rs +++ b/yjit/src/stats.rs @@ -332,7 +332,11 @@ make_counters! { send_ivar_set_method, send_zsuper_method, send_undef_method, - send_optimized_method_block_call, + send_bpp_dispatch, + send_bpp_not_simple, + send_bpp_megamorphic, + send_bpp_not_iseq_block, + send_bpp_arg0_splat, send_call_block, send_call_kwarg, send_call_multi_ractor, From 6adf5e3d0ae1062a72ca37843ec1c0f881ccb55f Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 6 Jul 2026 12:22:39 -0400 Subject: [PATCH 3/9] ZJIT: Fix extended basic block typo (#17679) This PR fixes a comment that calls ZJIT basic blocks extended. They have been traditional basic blocks since #16888. --- zjit/src/hir.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index dd4bbba8f25a65..92bfa09f999c36 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2322,7 +2322,7 @@ impl std::fmt::Display for Insn { } } -/// An extended basic block in a [`Function`]. +/// A basic block in a [`Function`]. #[derive(Default, Debug)] pub struct Block { /// The index of the first YARV instruction for the Block in the ISEQ From 53443163ec51c847245dab48f3c5445a9cadc554 Mon Sep 17 00:00:00 2001 From: Hartley McGuire Date: Wed, 1 Jul 2026 14:36:57 -0400 Subject: [PATCH 4/9] [DOC] Fix Thread::Mutex#unlock doc It was [previously][1] moved from C to Ruby, but the doc on the Ruby method was copied from `#lock` while the correct doc was left in C. [1]: 07b2356a6ad314b9a7b2bb9fc0527b440f004faa --- thread_sync.c | 7 ------- thread_sync.rb | 6 +++--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/thread_sync.c b/thread_sync.c index f6c3dca7745b3a..07a578bf56b764 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -504,13 +504,6 @@ do_mutex_unlock_safe(VALUE args) return Qnil; } -/* - * call-seq: - * mutex.unlock -> self - * - * Releases the lock. - * Raises +ThreadError+ if +mutex+ wasn't locked by the current thread. - */ VALUE rb_mutex_unlock(VALUE self) { diff --git a/thread_sync.rb b/thread_sync.rb index 18c7cc7adc9d7a..bab21788bc4a13 100644 --- a/thread_sync.rb +++ b/thread_sync.rb @@ -371,10 +371,10 @@ def try_lock end # call-seq: - # mutex.lock -> self + # mutex.unlock -> self # - # Attempts to grab the lock and waits if it isn't available. - # Raises +ThreadError+ if +mutex+ was locked by the current thread. + # Releases the lock. + # Raises +ThreadError+ if +mutex+ wasn't locked by the current thread. def unlock Primitive.rb_mut_unlock end From 9001320e7c4e974f3577f2e6f5f32d786b001618 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 6 Jul 2026 10:21:29 -0700 Subject: [PATCH 5/9] ZJIT: Rename po_from to post_order_from (#17681) We renamed `rpo` to `reverse_post_order` so rename this one to match as well. --- zjit/src/hir.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 92bfa09f999c36..feb1cedf834414 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5913,12 +5913,12 @@ impl Function { /// Return a traversal of the `Function`'s `BlockId`s in reverse post-order. pub fn reverse_post_order(&self) -> Vec { - let mut result = self.po_from(self.entries_block); + let mut result = self.post_order_from(self.entries_block); result.reverse(); result } - fn po_from(&self, start: BlockId) -> Vec { + fn post_order_from(&self, start: BlockId) -> Vec { #[derive(PartialEq)] enum Action { VisitEdges, From 082792a74fd56556104a99bb2430fd5ecb997a59 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 6 Jul 2026 11:10:17 -0700 Subject: [PATCH 6/9] ZJIT: Rename gen_save_pc_for_gc to gen_write_jit_frame (#17683) The old name is a bit of a misnomer now since it's not saving just the PC anymore. It's still one write, but it's the JITFrame pointer. --- zjit/src/codegen.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index a3cbade3609a3e..ed983bf315373b 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -61,7 +61,7 @@ struct JITState { iseq_calls: Vec, /// The number of native stack slots reserved for JITFrame, one per - /// simultaneously live frame (`inlining_depth() + 1`). gen_save_pc_for_gc() + /// simultaneously live frame (`inlining_depth() + 1`). gen_write_jit_frame() /// and the inlined frame push write a JITFrame into the slot selected by the /// current frame's depth. jit_frame_size: usize, @@ -402,7 +402,7 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func // Reserve one JITFrame slot per simultaneously live frame. The top-level // frame is depth 0, and each level of inlining adds another frame that // can be on the CFP chain at the same time, so we need - // `inlining_depth() + 1` slots. gen_save_pc_for_gc() and the inlined + // `inlining_depth() + 1` slots. gen_write_jit_frame() and the inlined // frame push select among these slots by the frame's depth, keeping each // frame's `cfp->jit_return` pointed at its own slot rather than a shared // one. @@ -1036,7 +1036,7 @@ fn gen_ccall_with_frame( // Can't use gen_prepare_non_leaf_call() because we need to adjust the SP // to account for the receiver and arguments (and block arguments if any) - gen_save_pc_for_gc(asm, state, 0); + gen_write_jit_frame(asm, state, 0); gen_save_sp(asm, caller_stack_size); gen_spill_stack(jit, asm, state); gen_spill_locals(jit, asm, state); @@ -1130,7 +1130,7 @@ fn gen_ccall_variadic( // Can't use gen_prepare_non_leaf_call() because we need to adjust the SP // to account for the receiver and arguments (like gen_ccall_with_frame does) - gen_save_pc_for_gc(asm, state, 0); + gen_write_jit_frame(asm, state, 0); gen_save_sp(asm, caller_stack_size); gen_spill_stack(jit, asm, state); gen_spill_locals(jit, asm, state); @@ -1549,7 +1549,7 @@ fn gen_push_inline_frame( // Save cfp->pc and cfp->sp for the caller frame. // Cannot use gen_prepare_non_leaf_call because we need special SP math. let stack_size = state.stack().len() - args.len() - 1; // -1 for receiver - gen_save_pc_for_gc(asm, state, 0); + gen_write_jit_frame(asm, state, 0); gen_save_sp(asm, stack_size); gen_spill_locals(jit, asm, state); @@ -1589,14 +1589,14 @@ fn gen_push_inline_frame( // Publish the inlined callee's entry JITFrame before the inlined body runs. // Frame walking functions such as rb_profile_frames can inspect the new - // CFP between this frame push and the first inlined gen_save_pc_for_gc, so + // CFP between this frame push and the first inlined gen_write_jit_frame, so // cfp->jit_return must already reference a valid JITFrame slot. Leaving it // stale or uninitialized is unsafe because CFP_ZJIT_FRAME has no independent // way to tell whether it points at a valid JITFrame slot. // // We install a pre-baked JITFrame for the callee's entry by writing its address into // the callee's own JITFrame slot and pointing the callee's cfp->jit_return at that - // slot, matching the protocol established by gen_entry_point + gen_save_pc_for_gc. + // slot, matching the protocol established by gen_entry_point + gen_write_jit_frame. // The callee runs one level deeper than the caller, so it uses the slot for // `state.depth + 1` (state is the caller's FrameState). Giving each inlining depth a // distinct slot keeps the caller's and callee's cfp->jit_return from aliasing the same @@ -1604,7 +1604,7 @@ fn gen_push_inline_frame( // one frame's PC/ISEQ into every aliased CFP on the chain. CFP_ZJIT_FRAME in zjit.h // reads the JITFrame via ((VALUE *)cfp->jit_return)[-1], so the field must be the // slot's address, not the JITFrame pointer itself. Once the inlined body runs its - // first gen_save_pc_for_gc, that call overwrites the same slot with a JITFrame + // first gen_write_jit_frame, that call overwrites the same slot with a JITFrame // carrying the current PC, just as the non-inlined path does. // // cfp->sp is left stale at frame push, matching the non-inlined gen_push_frame, which @@ -1686,7 +1686,7 @@ fn gen_send_iseq_direct( // Save cfp->pc and cfp->sp for the caller frame // Can't use gen_prepare_non_leaf_call because we need special SP math. let stack_size = state.stack().len() - args.len() - 1; // -1 for receiver - let jit_frame = gen_save_pc_for_gc(asm, state, stack_size); + let jit_frame = gen_write_jit_frame(asm, state, stack_size); gen_save_sp(asm, stack_size); gen_spill_locals(jit, asm, state); @@ -2913,7 +2913,7 @@ fn cfp_jit_return_for_depth(asm: &mut Assembler, depth: InlineDepth) -> Opnd { /// Save only the PC to CFP. Use this when you need to call gen_save_sp() /// immediately after with a custom stack size (e.g., gen_ccall_with_frame /// adjusts SP to exclude receiver and arguments). -fn gen_save_pc_for_gc(asm: &mut Assembler, state: &FrameState, stack_map_size: usize) -> *const zjit_jit_frame { +fn gen_write_jit_frame(asm: &mut Assembler, state: &FrameState, stack_map_size: usize) -> *const zjit_jit_frame { let opcode: usize = state.get_opcode().try_into().unwrap(); let next_pc: *const VALUE = unsafe { state.pc.offset(insn_len(opcode) as isize) }; @@ -2941,7 +2941,7 @@ fn gen_save_pc_for_gc(asm: &mut Assembler, state: &FrameState, stack_map_size: u /// However, to avoid marking uninitialized stack slots, this also updates SP, /// which may have cfp->sp for a past frame or a past non-leaf call. fn gen_prepare_call_with_gc(asm: &mut Assembler, state: &FrameState, leaf: bool, stack_map_size: usize) -> *const zjit_jit_frame { - let jit_frame = gen_save_pc_for_gc(asm, state, stack_map_size); + let jit_frame = gen_write_jit_frame(asm, state, stack_map_size); gen_save_sp(asm, state.stack_size()); if leaf { asm.expect_leaf_ccall(state.stack_size()); @@ -3006,7 +3006,7 @@ fn gen_spill_stack(jit: &JITState, asm: &mut Assembler, state: &FrameState) { /// writing stack slots. Otherwise spilling the stack can overwrite frame /// metadata below the real VM-stack base. fn gen_prepare_fallback_call(jit: &JITState, asm: &mut Assembler, state: &FrameState) { - gen_save_pc_for_gc(asm, state, 0); + gen_write_jit_frame(asm, state, 0); gen_save_sp(asm, state.stack_size()); gen_spill_locals(jit, asm, state); gen_spill_stack(jit, asm, state); @@ -3091,7 +3091,7 @@ fn gen_push_frame(asm: &mut Assembler, argc: usize, state: &FrameState, frame: C asm.mov(cfp_opnd(RUBY_OFFSET_CFP_BLOCK_CODE), 0.into()); } } else { - // C frames don't have a PC and ISEQ in normal operation. ISEQ frames set PC on gen_save_pc_for_gc(). + // C frames don't have a PC and ISEQ in normal operation. ISEQ frames set PC on gen_write_jit_frame(). // When runtime checks are enabled we poison the PC for C frames so accidental reads stand out. if let (None, Some(pc)) = (frame.iseq, PC_POISON) { asm.mov(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_PC), Opnd::const_ptr(pc)); From de811bc8ae4df996eb4d4e95d2f09ab84fffc944 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 6 Jul 2026 11:15:24 -0700 Subject: [PATCH 7/9] ZJIT: Don't side-effect in assert!() (#17678) This is a footgun, less bad than the C version, whereby an `assert!` could reasonably be changed into a `debug_assert!` and cause a problem because the condition no longer executes in release builds. This one would probably be instantly noticeable, but still. --- zjit/src/backend/lir.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 7321fc1877a06c..f4593984e89e20 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -2073,8 +2073,9 @@ impl Assembler } else { if let Some(allocation) = assignment[active_interval.id] { if let Some(reg) = allocation.alloc_pool_index(num_registers) { + let was_not_there_before = free_registers.insert(reg); assert!( - free_registers.insert(reg), + was_not_there_before, "attempted to return allocator register {:?} to the free pool more than once", allocation.assigned_reg().unwrap(), ); From 367befc489098ebc9af9e121fa0e855ef0d38a5f Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 6 Jul 2026 13:47:30 -0400 Subject: [PATCH 8/9] ZJIT: Move EP index/offset conversion functions to cruby Keep the functions together (not split in HIR/codegen) and in a more reasonable module. --- zjit/src/backend/lir.rs | 4 ++-- zjit/src/codegen.rs | 18 ----------------- zjit/src/cruby.rs | 43 +++++++++++++++++++++++++++++++++++++++++ zjit/src/hir.rs | 28 +-------------------------- 4 files changed, 46 insertions(+), 47 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index f4593984e89e20..cd9d2cfdc9df54 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -4,8 +4,8 @@ use std::fmt; use std::mem::take; use std::rc::Rc; use crate::bitset::BitSet; -use crate::codegen::{local_size_and_idx_to_ep_offset, perf_symbol_range_start, perf_symbol_range_end, register_with_perf}; -use crate::cruby::{IseqPtr, RUBY_OFFSET_CFP_ISEQ, RUBY_OFFSET_CFP_JIT_RETURN, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_I32, VALUE, ZJIT_STACK_MAP_SHIFT, ZJIT_STACK_MAP_VREG_TAG, vm_stack_canary, YarvInsnIdx, zjit_jit_frame}; +use crate::codegen::{perf_symbol_range_start, perf_symbol_range_end, register_with_perf}; +use crate::cruby::{IseqPtr, RUBY_OFFSET_CFP_ISEQ, RUBY_OFFSET_CFP_JIT_RETURN, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_I32, VALUE, ZJIT_STACK_MAP_SHIFT, ZJIT_STACK_MAP_VREG_TAG, vm_stack_canary, YarvInsnIdx, zjit_jit_frame, local_size_and_idx_to_ep_offset}; use crate::hir::{Invariant, SideExitReason}; use crate::hir; use crate::options::{TraceExits, PerfMap, get_option}; diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index ed983bf315373b..e4175adfa79245 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -3128,24 +3128,6 @@ fn gen_stack_overflow_check(jit: &mut JITState, asm: &mut Assembler, state: &Fra asm.jbe(jit, side_exit(jit, state, StackOverflow)); } - -/// Inverse of ep_offset_to_local_idx(). See ep_offset_to_local_idx() for details. -pub fn local_idx_to_ep_offset(iseq: IseqPtr, local_idx: usize) -> i32 { - let local_size = unsafe { get_iseq_body_local_table_size(iseq) }; - local_size_and_idx_to_ep_offset(local_size.to_usize(), local_idx) -} - -/// Convert the number of locals and a local index to an offset from the EP -pub fn local_size_and_idx_to_ep_offset(local_size: usize, local_idx: usize) -> i32 { - local_size as i32 - local_idx as i32 - 1 + VM_ENV_DATA_SIZE as i32 -} - -/// Convert the number of locals and a local index to an offset from the BP. -/// We don't move the SP register after entry, so we often use SP as BP. -pub fn local_size_and_idx_to_bp_offset(local_size: usize, local_idx: usize) -> i32 { - local_size_and_idx_to_ep_offset(local_size, local_idx) + 1 -} - /// Convert ISEQ into High-level IR fn compile_iseq(iseq: IseqPtr) -> Result { // Convert ZJIT instructions back to bare instructions diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 3b6f5bc5cb138f..4cf55f6de4f8d7 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -337,6 +337,49 @@ pub fn iseq_rest_param_idx(params: &IseqParameters) -> Option { } } +/// Compute the index of a local variable from its slot index +pub fn ep_offset_to_local_idx(iseq: IseqPtr, ep_offset: u32) -> usize { + // Layout illustration + // This is an array of VALUE + // | VM_ENV_DATA_SIZE | + // v v + // low addr <+-------+-------+-------+-------+------------------+ + // |local 0|local 1| ... |local n| .... | + // +-------+-------+-------+-------+------------------+ + // ^ ^ ^ ^ + // +-------+---local_table_size----+ cfp->ep--+ + // | | + // +------------------ep_offset---------------+ + // + // See usages of local_var_name() from iseq.c for similar calculation. + + // Equivalent of iseq->body->local_table_size + let local_table_size: i32 = unsafe { get_iseq_body_local_table_size(iseq) } + .try_into() + .unwrap(); + let op = (ep_offset - VM_ENV_DATA_SIZE) as i32; + let local_idx = local_table_size - op - 1; + assert!(local_idx >= 0 && local_idx < local_table_size); + local_idx.try_into().unwrap() +} + +/// Inverse of ep_offset_to_local_idx(). See ep_offset_to_local_idx() for details. +pub fn local_idx_to_ep_offset(iseq: IseqPtr, local_idx: usize) -> i32 { + let local_size = unsafe { get_iseq_body_local_table_size(iseq) }; + local_size_and_idx_to_ep_offset(local_size.to_usize(), local_idx) +} + +/// Convert the number of locals and a local index to an offset from the EP +pub fn local_size_and_idx_to_ep_offset(local_size: usize, local_idx: usize) -> i32 { + local_size as i32 - local_idx as i32 - 1 + VM_ENV_DATA_SIZE as i32 +} + +/// Convert the number of locals and a local index to an offset from the BP. +/// We don't move the SP register after entry, so we often use SP as BP. +pub fn local_size_and_idx_to_bp_offset(local_size: usize, local_idx: usize) -> i32 { + local_size_and_idx_to_ep_offset(local_size, local_idx) + 1 +} + /// Iterate over all existing ISEQs pub fn for_each_iseq(mut callback: F) { unsafe extern "C" fn callback_wrapper(iseq: IseqPtr, data: *mut c_void) { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index feb1cedf834414..8d6e0055089dae 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -7,7 +7,7 @@ #![allow(clippy::match_like_matches_macro)] use crate::{ backend::lir::C_ARG_OPNDS, - cast::IntoUsize, codegen::{local_idx_to_ep_offset, max_iseq_versions}, cruby::*, invariants::{self, iseq_seen_ep_escape}, payload::get_or_create_iseq_payload, options::{debug, get_option, DumpHIR, InlineDepth}, state::ZJITState, json::Json, + cast::IntoUsize, codegen::{max_iseq_versions}, cruby::*, invariants::{self, iseq_seen_ep_escape}, payload::get_or_create_iseq_payload, options::{debug, get_option, DumpHIR, InlineDepth}, state::ZJITState, json::Json, state, }; use std::{ @@ -6830,32 +6830,6 @@ pub struct FrameStatePrinter<'a> { ptr_map: &'a PtrPrintMap, } -/// Compute the index of a local variable from its slot index -fn ep_offset_to_local_idx(iseq: IseqPtr, ep_offset: u32) -> usize { - // Layout illustration - // This is an array of VALUE - // | VM_ENV_DATA_SIZE | - // v v - // low addr <+-------+-------+-------+-------+------------------+ - // |local 0|local 1| ... |local n| .... | - // +-------+-------+-------+-------+------------------+ - // ^ ^ ^ ^ - // +-------+---local_table_size----+ cfp->ep--+ - // | | - // +------------------ep_offset---------------+ - // - // See usages of local_var_name() from iseq.c for similar calculation. - - // Equivalent of iseq->body->local_table_size - let local_table_size: i32 = unsafe { get_iseq_body_local_table_size(iseq) } - .try_into() - .unwrap(); - let op = (ep_offset - VM_ENV_DATA_SIZE) as i32; - let local_idx = local_table_size - op - 1; - assert!(local_idx >= 0 && local_idx < local_table_size); - local_idx.try_into().unwrap() -} - impl FrameState { fn new(iseq: IseqPtr) -> FrameState { FrameState { iseq, pc: std::ptr::null::(), insn_idx: 0, stack: vec![], locals: vec![], caller: None, depth: 0 } From e7a2126f94f908d179fd0ba42619ccec4778d359 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 6 Jul 2026 14:06:26 -0400 Subject: [PATCH 9/9] ZJIT: Make name rustdoc-able --- zjit/src/cruby.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 4cf55f6de4f8d7..e68890fa9f368c 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -363,7 +363,7 @@ pub fn ep_offset_to_local_idx(iseq: IseqPtr, ep_offset: u32) -> usize { local_idx.try_into().unwrap() } -/// Inverse of ep_offset_to_local_idx(). See ep_offset_to_local_idx() for details. +/// Inverse of ep_offset_to_local_idx(). See [`ep_offset_to_local_idx`] for details. pub fn local_idx_to_ep_offset(iseq: IseqPtr, local_idx: usize) -> i32 { let local_size = unsafe { get_iseq_body_local_table_size(iseq) }; local_size_and_idx_to_ep_offset(local_size.to_usize(), local_idx)