From b04586c210bf9f0ff8cd35a8a917edb3e78af9e5 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:30:05 +0100 Subject: [PATCH 01/22] [ruby/prism] Implement various string start/end events for ripper `tstring_beg` in particular is needed by `yard`. Before: > 1980 examples, 606 failures, 15 pending After: > 1980 examples, 582 failures, 15 pending Thought it would be more, but oh well. It needs `on_sp` which I guess is why there are not many new passes https://github.com/ruby/prism/commit/e1b18cb582 --- lib/prism/translation/ripper.rb | 243 ++++++++++++++++++-------------- test/prism/ruby/ripper_test.rb | 3 +- 2 files changed, 136 insertions(+), 110 deletions(-) diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb index e1dec0b47e16de..97abf74083a68e 100644 --- a/lib/prism/translation/ripper.rb +++ b/lib/prism/translation/ripper.rb @@ -35,9 +35,6 @@ module Translation # - on_rparen # - on_semicolon # - on_sp - # - on_symbeg - # - on_tstring_beg - # - on_tstring_end # class Ripper < Compiler # Parses the given Ruby program read from +src+. @@ -2235,61 +2232,67 @@ def visit_interpolated_regular_expression_node(node) # "foo #{bar}" # ^^^^^^^^^^^^ def visit_interpolated_string_node(node) - if node.opening&.start_with?("<<~") - heredoc = visit_heredoc_string_node(node) + with_string_bounds(node) do + if node.opening&.start_with?("<<~") + heredoc = visit_heredoc_string_node(node) - bounds(node.location) - on_string_literal(heredoc) - elsif !node.heredoc? && node.parts.length > 1 && node.parts.any? { |part| (part.is_a?(StringNode) || part.is_a?(InterpolatedStringNode)) && !part.opening_loc.nil? } - first, *rest = node.parts - rest.inject(visit(first)) do |content, part| - concat = visit(part) - - bounds(part.location) - on_string_concat(content, concat) - end - else - bounds(node.parts.first.location) - parts = - node.parts.inject(on_string_content) do |content, part| - on_string_add(content, visit_string_content(part)) + bounds(node.location) + on_string_literal(heredoc) + elsif !node.heredoc? && node.parts.length > 1 && node.parts.any? { |part| (part.is_a?(StringNode) || part.is_a?(InterpolatedStringNode)) && !part.opening_loc.nil? } + first, *rest = node.parts + rest.inject(visit(first)) do |content, part| + concat = visit(part) + + bounds(part.location) + on_string_concat(content, concat) end + else + bounds(node.parts.first.location) + parts = + node.parts.inject(on_string_content) do |content, part| + on_string_add(content, visit_string_content(part)) + end - bounds(node.location) - on_string_literal(parts) + bounds(node.location) + on_string_literal(parts) + end end end # :"foo #{bar}" # ^^^^^^^^^^^^^ def visit_interpolated_symbol_node(node) - bounds(node.parts.first.location) - parts = - node.parts.inject(on_string_content) do |content, part| - on_string_add(content, visit_string_content(part)) - end + with_string_bounds(node) do + bounds(node.parts.first.location) + parts = + node.parts.inject(on_string_content) do |content, part| + on_string_add(content, visit_string_content(part)) + end - bounds(node.location) - on_dyna_symbol(parts) + bounds(node.location) + on_dyna_symbol(parts) + end end # `foo #{bar}` # ^^^^^^^^^^^^ def visit_interpolated_x_string_node(node) - if node.opening.start_with?("<<~") - heredoc = visit_heredoc_x_string_node(node) + with_string_bounds(node) do + if node.opening.start_with?("<<~") + heredoc = visit_heredoc_x_string_node(node) - bounds(node.location) - on_xstring_literal(heredoc) - else - bounds(node.parts.first.location) - parts = - node.parts.inject(on_xstring_new) do |content, part| - on_xstring_add(content, visit_string_content(part)) - end + bounds(node.location) + on_xstring_literal(heredoc) + else + bounds(node.parts.first.location) + parts = + node.parts.inject(on_xstring_new) do |content, part| + on_xstring_add(content, visit_string_content(part)) + end - bounds(node.location) - on_xstring_literal(parts) + bounds(node.location) + on_xstring_literal(parts) + end end end @@ -3022,24 +3025,60 @@ def visit_statements_node(node) # "foo" # ^^^^^ def visit_string_node(node) - if (content = node.content).empty? - bounds(node.location) - on_string_literal(on_string_content) - elsif (opening = node.opening) == "?" - bounds(node.location) - on_CHAR("?#{node.content}") - elsif opening.start_with?("<<~") - heredoc = visit_heredoc_string_node(node.to_interpolated) + with_string_bounds(node) do + if (content = node.content).empty? + bounds(node.location) + on_string_literal(on_string_content) + elsif (opening = node.opening) == "?" + bounds(node.location) + on_CHAR("?#{node.content}") + elsif opening.start_with?("<<~") + heredoc = visit_heredoc_string_node(node.to_interpolated) - bounds(node.location) - on_string_literal(heredoc) - else - bounds(node.content_loc) - tstring_content = on_tstring_content(content) + bounds(node.location) + on_string_literal(heredoc) + else + bounds(node.content_loc) + tstring_content = on_tstring_content(content) - bounds(node.location) - on_string_literal(on_string_add(on_string_content, tstring_content)) + bounds(node.location) + on_string_literal(on_string_add(on_string_content, tstring_content)) + end + end + end + + # Responsible for emitting the various string-like begin/end events + private def with_string_bounds(node) + # `foo "bar": baz` doesn't emit the closing location + assoc = !(opening = node.opening)&.include?(":") && node.closing&.end_with?(":") + + is_heredoc = opening&.start_with?("<<") + if is_heredoc + bounds(node.opening_loc) + on_heredoc_beg(node.opening) + elsif opening&.start_with?(":", "%s") + bounds(node.opening_loc) + on_symbeg(node.opening) + elsif opening&.start_with?("`", "%x") + bounds(node.opening_loc) + on_backtick(node.opening) + elsif opening && !opening.start_with?("?") + bounds(node.opening_loc) + on_tstring_beg(opening) end + + result = yield + return result if assoc + + if is_heredoc + bounds(node.closing_loc) + on_heredoc_end(node.closing) + elsif node.closing_loc + bounds(node.closing_loc) + on_tstring_end(node.closing) + end + + result end # Ripper gives back the escaped string content but strips out the common @@ -3119,36 +3158,18 @@ def visit_string_node(node) # Visit a heredoc node that is representing a string. private def visit_heredoc_string_node(node) - bounds(node.opening_loc) - on_heredoc_beg(node.opening) - bounds(node.location) - result = - visit_heredoc_node(node.parts, on_string_content) do |parts, part| - on_string_add(parts, part) - end - - bounds(node.closing_loc) - on_heredoc_end(node.closing) - - result + visit_heredoc_node(node.parts, on_string_content) do |parts, part| + on_string_add(parts, part) + end end # Visit a heredoc node that is representing an xstring. private def visit_heredoc_x_string_node(node) - bounds(node.opening_loc) - on_heredoc_beg(node.opening) - bounds(node.location) - result = - visit_heredoc_node(node.parts, on_xstring_new) do |parts, part| - on_xstring_add(parts, part) - end - - bounds(node.closing_loc) - on_heredoc_end(node.closing) - - result + visit_heredoc_node(node.parts, on_xstring_new) do |parts, part| + on_xstring_add(parts, part) + end end # super(foo) @@ -3175,23 +3196,25 @@ def visit_super_node(node) # :foo # ^^^^ def visit_symbol_node(node) - if node.value_loc.nil? - bounds(node.location) - on_dyna_symbol(on_string_content) - elsif (opening = node.opening)&.match?(/^%s|['"]:?$/) - bounds(node.value_loc) - content = on_string_add(on_string_content, on_tstring_content(node.value)) - bounds(node.location) - on_dyna_symbol(content) - elsif (closing = node.closing) == ":" - bounds(node.location) - on_label("#{node.value}:") - elsif opening.nil? && node.closing_loc.nil? - bounds(node.value_loc) - on_symbol_literal(visit_token(node.value)) - else - bounds(node.value_loc) - on_symbol_literal(on_symbol(visit_token(node.value))) + with_string_bounds(node) do + if node.value_loc.nil? + bounds(node.location) + on_dyna_symbol(on_string_content) + elsif (opening = node.opening)&.match?(/^%s|['"]:?$/) + bounds(node.value_loc) + content = on_string_add(on_string_content, on_tstring_content(node.value)) + bounds(node.location) + on_dyna_symbol(content) + elsif (closing = node.closing) == ":" + bounds(node.location) + on_label("#{node.value}:") + elsif opening.nil? && node.closing_loc.nil? + bounds(node.value_loc) + on_symbol_literal(visit_token(node.value)) + else + bounds(node.value_loc) + on_symbol_literal(on_symbol(visit_token(node.value))) + end end end @@ -3314,20 +3337,22 @@ def visit_while_node(node) # `foo` # ^^^^^ def visit_x_string_node(node) - if node.unescaped.empty? - bounds(node.location) - on_xstring_literal(on_xstring_new) - elsif node.opening.start_with?("<<~") - heredoc = visit_heredoc_x_string_node(node.to_interpolated) + with_string_bounds(node) do + if node.unescaped.empty? + bounds(node.location) + on_xstring_literal(on_xstring_new) + elsif node.opening.start_with?("<<~") + heredoc = visit_heredoc_x_string_node(node.to_interpolated) - bounds(node.location) - on_xstring_literal(heredoc) - else - bounds(node.content_loc) - content = on_tstring_content(node.content) + bounds(node.location) + on_xstring_literal(heredoc) + else + bounds(node.content_loc) + content = on_tstring_content(node.content) - bounds(node.location) - on_xstring_literal(on_xstring_add(on_xstring_new, content)) + bounds(node.location) + on_xstring_literal(on_xstring_add(on_xstring_new, content)) + end end end diff --git a/test/prism/ruby/ripper_test.rb b/test/prism/ruby/ripper_test.rb index 8c80b9f886969f..85a52e2bb951b3 100644 --- a/test/prism/ruby/ripper_test.rb +++ b/test/prism/ruby/ripper_test.rb @@ -106,6 +106,7 @@ class RipperTest < TestCase "variables.txt", "whitequark/dedenting_heredoc.txt", "whitequark/masgn_nested.txt", + "whitequark/newline_in_hash_argument.txt", "whitequark/numparam_ruby_bug_19025.txt", "whitequark/op_asgn_cmd.txt", "whitequark/parser_drops_truncated_parts_of_squiggly_heredoc.txt", @@ -135,7 +136,7 @@ def test_lex_ignored_missing_heredoc_end end end - UNSUPPORTED_EVENTS = %i[backtick comma heredoc_beg heredoc_end ignored_nl kw label_end lbrace lbracket lparen nl op rbrace rbracket rparen semicolon sp symbeg tstring_beg tstring_end words_sep ignored_sp] + UNSUPPORTED_EVENTS = %i[comma ignored_nl kw label_end lbrace lbracket lparen nl op rbrace rbracket rparen semicolon sp words_sep ignored_sp] SUPPORTED_EVENTS = Translation::Ripper::EVENTS - UNSUPPORTED_EVENTS module Events From 5eff7e0ec2ab405be1db7a57819d50ddf75008e2 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:18:34 +0100 Subject: [PATCH 02/22] [ruby/prism] Also handle string conversion in `Ripper.lex` In `ripper`, both go through the same converion logic. Needed for rspec, no other failures in their own tests https://github.com/ruby/prism/commit/510258aa2b --- lib/prism/translation/ripper.rb | 29 +++++++++++++++++------------ test/prism/ruby/ripper_test.rb | 8 ++++++++ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb index 97abf74083a68e..bbfa1f4d05d175 100644 --- a/lib/prism/translation/ripper.rb +++ b/lib/prism/translation/ripper.rb @@ -66,7 +66,7 @@ def self.parse(src, filename = "(ripper)", lineno = 1) # [[1, 13], :on_kw, "end", END ]] # def self.lex(src, filename = "-", lineno = 1, raise_errors: false) - result = Prism.lex_compat(src, filepath: filename, line: lineno, version: "current") + result = Prism.lex_compat(coerce_source(src), filepath: filename, line: lineno, version: "current") if result.failure? && raise_errors raise SyntaxError, result.errors.first.message @@ -88,6 +88,21 @@ def self.tokenize(...) lex(...).map { |token| token[2] } end + # Mirros the various lex_types that ripper supports + def self.coerce_source(source) # :nodoc: + if source.is_a?(IO) + source.read + elsif source.respond_to?(:gets) + src = +"" + while line = source.gets + src << line + end + src + else + source.to_str + end + end + # This contains a table of all of the parser events and their # corresponding arity. PARSER_EVENT_TABLE = { @@ -477,17 +492,7 @@ def self.lex_state_name(state) # Create a new Translation::Ripper object with the given source. def initialize(source, filename = "(ripper)", lineno = 1) - if source.is_a?(IO) - @source = source.read - elsif source.respond_to?(:gets) - @source = +"" - while line = source.gets - @source << line - end - else - @source = source.to_str - end - + @source = Ripper.coerce_source(source) @filename = filename @lineno = lineno @column = 0 diff --git a/test/prism/ruby/ripper_test.rb b/test/prism/ruby/ripper_test.rb index 85a52e2bb951b3..7274454e1b44ed 100644 --- a/test/prism/ruby/ripper_test.rb +++ b/test/prism/ruby/ripper_test.rb @@ -244,6 +244,14 @@ def string_like.to_str end end + def test_lex_coersion + string_like = Object.new + def string_like.to_str + "a" + end + assert_equal Ripper.lex(string_like), Translation::Ripper.lex(string_like) + end + # Check that the hardcoded values don't change without us noticing. def test_internals actual = Translation::Ripper.constants.select { |name| name.start_with?("EXPR_") }.sort From 851b8f852313c361465cf760701e23db0ea4d474 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 26 Mar 2026 14:44:56 -0700 Subject: [PATCH 03/22] Remove class alloc check This checks that the value returned from the function registered with rb_define_alloc_func is of the correct class. When this was first introduced in 1fe40b7cc5 (by Matz on 2001-10-03), allocation was done via user-defined Object#allocate, so it made sense to have a runtime check in release builds. Now that it's defined via rb_define_alloc_func in the C extension API, I don't think it's necessary. The check is surprisingly expensive. Removing it makes Object.new about 10% faster. It allows the C compiler to optimize the call to the function pointer as a tail-call. Removing this also allows ZJIT/YJIT to call the function directly (ZJIT already does this by having a list of known safe allocation functions). There's no way for users to ever have seen this check, other than by writing a misbehaving C extension, which returns objects with the wrong class. [Feature #21966] --- object.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/object.c b/object.c index c3241a198d369a..cb21741f315ec3 100644 --- a/object.c +++ b/object.c @@ -2337,11 +2337,7 @@ class_call_alloc_func(rb_alloc_func_t allocator, VALUE klass) obj = (*allocator)(klass); - if (UNLIKELY(RBASIC_CLASS(obj) != klass)) { - if (rb_obj_class(obj) != rb_class_real(klass)) { - rb_raise(rb_eTypeError, "wrong instance allocation"); - } - } + RUBY_ASSERT(rb_obj_class(obj) == rb_class_real(klass)); return obj; } From 54d58909b579b4f97bca8fb131bf988e3eacdd84 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 27 Mar 2026 09:06:40 -0700 Subject: [PATCH 04/22] ZJIT: Fix profile_stack to skip block arg for ARGS_BLOCKARG sends (#16581) For sends with ARGS_BLOCKARG (e.g. `foo(&block)`), the block arg sits on the stack above the receiver and regular arguments. The profiling (both interpreter and exit profiling) only records types for the receiver and regular args (argc + 1 values), but profile_stack was mapping those types onto stack positions starting from the top, which incorrectly mapped them onto the block arg and the wrong operands. Fix by detecting ARGS_BLOCKARG at the profile_stack call site and passing a stack_offset of 1 to skip the block arg. This allows resolve_receiver_type_from_profile to find the correct receiver type, enabling method dispatch optimization for these sends after recompilation via exit profiling. --- zjit/src/hir.rs | 20 ++++++++++++++++---- zjit/src/hir/opt_tests.rs | 40 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 591ca0aad7cf22..9227bbd730dbb2 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -6758,15 +6758,17 @@ impl ProfileOracle { Self { payload, types: Default::default() } } - /// Map the interpreter-recorded types of the stack onto the HIR operands on our compile-time virtual stack - fn profile_stack(&mut self, state: &FrameState) { + /// Map the interpreter-recorded types of the stack onto the HIR operands on our compile-time virtual stack. + /// `stack_offset` is the number of extra stack entries above the profiled operands (e.g. 1 for + /// sends with ARGS_BLOCKARG, where the block arg sits on top of the regular args). + fn profile_stack(&mut self, state: &FrameState, stack_offset: usize) { let iseq_insn_idx = state.insn_idx; let Some(operand_types) = self.payload.profile.get_operand_types(iseq_insn_idx) else { return }; let entry = self.types.entry(iseq_insn_idx).or_default(); // operand_types is always going to be <= stack size (otherwise it would have an underflow // at run-time) so use that to drive iteration. for (idx, insn_type_distribution) in operand_types.iter().rev().enumerate() { - let insn = state.stack_topn(idx).expect("Unexpected stack underflow in profiling"); + let insn = state.stack_topn(idx + stack_offset).expect("Unexpected stack underflow in profiling"); entry.push((insn, TypeDistributionSummary::new(insn_type_distribution))) } } @@ -6969,7 +6971,17 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } } else { - profiles.profile_stack(&exit_state); + // For sends with ARGS_BLOCKARG, the block arg sits on the stack above + // the profiled operands (receiver + regular args). Skip it so that the + // profile types map onto the correct HIR operands. + let stack_offset = if opcode == YARVINSN_send || opcode == YARVINSN_opt_send_without_block { + let cd: *const rb_call_data = get_arg(pc, 0).as_ptr(); + let flags = unsafe { vm_ci_flag(rb_get_call_data_ci(cd)) }; + usize::from(flags & VM_CALL_ARGS_BLOCKARG != 0) + } else { + 0 + }; + profiles.profile_stack(&exit_state, stack_offset); } // Flag a future getlocal/setlocal to add a patch point if this instruction is not leaf. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 2d3e43d0f1978b..c7b2caf31aaf86 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -11427,12 +11427,50 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v11:StaticSymbol[:the_block] = Const Value(VALUE(0x1000)) - v13:BasicObject = Send v6, 0x1008, :callee, v11 # SendFallbackReason: Send: no profile data available + v13:BasicObject = Send v6, 0x1008, :callee, v11 # SendFallbackReason: Complex argument passing CheckInterrupts Return v13 "); } + #[test] + fn test_profile_stack_skips_block_arg() { + // Regression test: profile_stack must skip the &block arg on the stack when mapping + // profiled operand types. Without the fix, the receiver type would be mapped to the + // wrong stack slot, causing resolve_receiver_type to return NoProfile. + // With the fix, the receiver type is correctly resolved and the send gets past type + // resolution to hit the ARGS_BLOCKARG guard (ComplexArgPass) instead of NoProfile. + eval(" + def test(&block) = [].map(&block) + test { |x| x }; test { |x| x } + "); + 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): + 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, 0x1001, :map, v21 # SendFallbackReason: Complex argument passing + CheckInterrupts + Return v23 + "); + } + #[test] fn test_optimize_stringexact_eq_stringexact() { eval(r#" From 6b3cd4875ca8e368e6b649f8ecc7610f1e68fd3d Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 27 Mar 2026 09:10:25 -0700 Subject: [PATCH 05/22] ZJIT: Check native stack before compiling ISEQs (#16576) * ZJIT: Check native stack before compiling ISEQ When the native/machine stack is nearly exhausted, don't compile and enter ZJIT code. JIT-compiled code uses more native stack per call frame than the interpreter, so falling back to the interpreter avoids SystemStackError in cases where the interpreter would still have room. This matches what YJIT does in rb_yjit_iseq_gen_entry_point(). * ZJIT: Add skipped_native_stack_full counter --- vm.c | 4 ++-- zjit.c | 6 +++--- zjit.h | 4 ++-- zjit/src/codegen.rs | 8 +++++++- zjit/src/stats.rs | 1 + 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/vm.c b/vm.c index 916e379d671a1e..3825189f38ebac 100644 --- a/vm.c +++ b/vm.c @@ -550,7 +550,7 @@ zjit_compile(rb_execution_context_t *ec) // At call-threshold, compile the ISEQ with ZJIT. if (body->jit_entry_calls == rb_zjit_call_threshold) { - rb_zjit_compile_iseq(iseq, false); + rb_zjit_compile_iseq(iseq, ec, false); } } return body->jit_entry; @@ -610,7 +610,7 @@ jit_compile_exception(rb_execution_context_t *ec) // At call-threshold, compile the ISEQ with ZJIT. if (body->jit_exception_calls == rb_zjit_call_threshold) { - rb_zjit_compile_iseq(iseq, true); + rb_zjit_compile_iseq(iseq, ec, true); } } #endif diff --git a/zjit.c b/zjit.c index f9b029503aa8f4..fdebc922f0fc85 100644 --- a/zjit.c +++ b/zjit.c @@ -35,14 +35,14 @@ enum zjit_struct_offsets { void rb_zjit_profile_disable(const rb_iseq_t *iseq); void -rb_zjit_compile_iseq(const rb_iseq_t *iseq, bool jit_exception) +rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception) { RB_VM_LOCKING() { rb_vm_barrier(); // Compile a block version starting at the current instruction - uint8_t *rb_zjit_iseq_gen_entry_point(const rb_iseq_t *iseq, bool jit_exception); // defined in Rust - uintptr_t code_ptr = (uintptr_t)rb_zjit_iseq_gen_entry_point(iseq, jit_exception); + uint8_t *rb_zjit_iseq_gen_entry_point(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception); // defined in Rust + uintptr_t code_ptr = (uintptr_t)rb_zjit_iseq_gen_entry_point(iseq, ec, jit_exception); if (jit_exception) { iseq->body->jit_exception = (rb_jit_func_t)code_ptr; diff --git a/zjit.h b/zjit.h index f42b77cb356dac..d1d1b01df3b68e 100644 --- a/zjit.h +++ b/zjit.h @@ -13,7 +13,7 @@ extern void *rb_zjit_entry; extern uint64_t rb_zjit_call_threshold; extern uint64_t rb_zjit_profile_threshold; -void rb_zjit_compile_iseq(const rb_iseq_t *iseq, bool jit_exception); +void rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception); void rb_zjit_profile_insn(uint32_t insn, rb_execution_context_t *ec); void rb_zjit_profile_enable(const rb_iseq_t *iseq); void rb_zjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop); @@ -31,7 +31,7 @@ void rb_zjit_invalidate_no_singleton_class(VALUE klass); void rb_zjit_invalidate_root_box(void); #else #define rb_zjit_entry 0 -static inline void rb_zjit_compile_iseq(const rb_iseq_t *iseq, bool jit_exception) {} +static inline void rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception) {} static inline void rb_zjit_profile_insn(uint32_t insn, rb_execution_context_t *ec) {} static inline void rb_zjit_profile_enable(const rb_iseq_t *iseq) {} static inline void rb_zjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop) {} diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 2f2454f41e362a..3a6d60af2df471 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -144,7 +144,13 @@ define_split_jumps! { /// If jit_exception is true, compile JIT code for handling exceptions. /// See jit_compile_exception() for details. #[unsafe(no_mangle)] -pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, jit_exception: bool) -> *const u8 { +pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exception: bool) -> *const u8 { + // Don't compile when there is insufficient native stack space + if unsafe { rb_ec_stack_check(ec as _) } != 0 { + incr_counter!(skipped_native_stack_full); + return std::ptr::null(); + } + // Take a lock to avoid writing to ISEQ in parallel with Ractors. // with_vm_lock() does nothing if the program doesn't use Ractors. with_vm_lock(src_loc!(), || { diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index bd36464bb73a32..b2e67afb166b9c 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -156,6 +156,7 @@ make_counters! { default { compiled_iseq_count, failed_iseq_count, + skipped_native_stack_full, compile_time_ns, profile_time_ns, From 7b85b2143273f437d475e19d87f3cbb5c95842ea Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 27 Mar 2026 09:29:39 -0700 Subject: [PATCH 06/22] Bump irb version to fix a flaky test https://github.com/ruby/irb/pull/1191 --- gems/bundled_gems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/bundled_gems b/gems/bundled_gems index f0d7da6108986c..d68c27f01eaa0b 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -39,7 +39,7 @@ benchmark 0.5.0 https://github.com/ruby/benchmark logger 1.7.0 https://github.com/ruby/logger rdoc 7.2.0 https://github.com/ruby/rdoc 911b122a587e24f05434dbeb2c3e39cea607e21f win32ole 1.9.3 https://github.com/ruby/win32ole -irb 1.17.0 https://github.com/ruby/irb +irb 1.17.0 https://github.com/ruby/irb cfd0b917d3feb01adb7d413b19faeb0309900599 reline 0.6.3 https://github.com/ruby/reline readline 0.0.4 https://github.com/ruby/readline fiddle 1.1.8 https://github.com/ruby/fiddle From 06f746fd8abd2ced4b3159171c52614b90ef37c2 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 26 Mar 2026 22:38:27 -0700 Subject: [PATCH 07/22] ZJIT: Skip convert_no_profile_sends on the final ISEQ version When an ISEQ has already reached MAX_ISEQ_VERSIONS, converting no-profile sends to SideExits is counterproductive: the exit fires every time but can never trigger recompilation. Keep them as Send fallbacks so the interpreter handles them directly without the overhead of a SideExit + no_profile_send_recompile call. --- zjit/src/hir.rs | 15 ++++++++ zjit/src/hir/opt_tests.rs | 74 +++++++++++---------------------------- 2 files changed, 36 insertions(+), 53 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 9227bbd730dbb2..b5c5fffaa709c0 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2372,6 +2372,10 @@ pub struct Function { /// Whether previously, a function for this ISEQ was invalidated due to /// singleton class creation (violation of NoSingletonClass invariant). was_invalidated_for_singleton_class_creation: bool, + /// Whether this is the last allowed version for this ISEQ (at MAX_ISEQ_VERSIONS). + /// When true, convert_no_profile_sends skips converting sends to SideExits since + /// no further recompilation is possible. + is_final_version: bool, /// The types for the parameters of this function. They are copied to the type /// of entry block params after infer_types() fills Empty to all insn_types. param_types: Vec, @@ -2479,6 +2483,7 @@ impl Function { Function { iseq, was_invalidated_for_singleton_class_creation: false, + is_final_version: false, insns: vec![], insn_types: vec![], union_find: UnionFind::new().into(), @@ -5031,6 +5036,12 @@ impl Function { /// The remaining no-profile sends are turned into side exits that trigger recompilation with /// fresh profile data. fn convert_no_profile_sends(&mut self) { + // On the final version, recompilation is not possible, so converting sends to + // SideExits would just add overhead (the exit fires every time without benefit). + // Keep them as Send fallbacks so the interpreter handles them directly. + if self.is_final_version { + return; + } for block in self.rpo() { let old_insns = std::mem::take(&mut self.blocks[block.0].insns); assert!(self.blocks[block.0].insns.is_empty()); @@ -6816,6 +6827,10 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let mut profiles = ProfileOracle::new(payload); let mut fun = Function::new(iseq); fun.was_invalidated_for_singleton_class_creation = payload.was_invalidated_for_singleton_class_creation; + // invalidate_iseq_version only invalidates when versions.len() < MAX_ISEQ_VERSIONS. + // After this compilation, versions.len() will be current + 1. If that reaches the limit, + // exit profiling can't trigger another recompile, so SideExits would fire permanently. + fun.is_final_version = payload.versions.len() + 1 >= crate::codegen::MAX_ISEQ_VERSIONS; // Compute a map of PC->Block by finding jump targets let jit_entry_insns = jit_entry_insns(iseq); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index c7b2caf31aaf86..96882445edbeb6 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -14962,62 +14962,32 @@ mod hir_opt_tests { } #[test] - fn test_recompile_no_profile_send() { - // Define a callee method and a test method that calls it + fn test_no_profile_send_on_final_version() { + // On the final ISEQ version (MAX_ISEQ_VERSIONS reached), no-profile sends should + // remain as Send fallbacks instead of being converted to SideExits, since recompilation + // is no longer possible and SideExits would fire every time without benefit. + // + // Use call_threshold=3 to ensure the method is auto-compiled before hir_string() builds + // the HIR. The auto-compile creates version 1, and hir_string() creates version 2 + // (= MAX_ISEQ_VERSIONS), so is_final_version is true. + set_call_threshold(3); eval(" - def greet_recompile(x) = x.to_s - def test_no_profile_recompile(flag) + def greet_final(x) = x.to_s + def test_final_version(flag) if flag - greet_recompile(42) + greet_final(42) else 'hello' end end "); + // Call enough times to trigger auto-compilation. flag=false so greet_final is never + // reached and has no profile data. + eval("3.times { test_final_version(false) }"); - // With call_threshold=2, num_profiles=1: - // 1st call profiles (flag=false, so greet is never reached) - // 2nd call compiles (greet has no profile data -> SideExit recompile) - eval("test_no_profile_recompile(false); test_no_profile_recompile(false)"); - - // The first compilation should have SideExit NoProfileSend recompile - // for the greet_recompile(42) callsite since it was never profiled. - assert_snapshot!(hir_string("test_no_profile_recompile"), @r" - fn test_no_profile_recompile@:4: - bb1(): - EntryPoint interpreter - v1:BasicObject = LoadSelf - v2:CPtr = LoadSP - v3:BasicObject = LoadField v2, :flag@0x1000 - Jump bb3(v1, v3) - bb2(): - EntryPoint JIT(0) - v6:BasicObject = LoadArg :self@0 - v7:BasicObject = LoadArg :flag@1 - Jump bb3(v6, v7) - bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - IfFalse v16, bb4(v9, v17) - v19:Truthy = RefineType v10, Truthy - v23:Fixnum[42] = Const Value(42) - SideExit NoProfileSend recompile - bb4(v30:BasicObject, v31:Falsy): - v35:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v36:StringExact = StringCopy v35 - CheckInterrupts - Return v36 - "); - - // Now call with flag=true. This hits the SideExit, which profiles - // the send and invalidates the ISEQ for recompilation. - eval("test_no_profile_recompile(true)"); - - // After profiling via the side exit, rebuilding HIR should now - // have a SendDirect for greet_recompile instead of SideExit. - assert_snapshot!(hir_string("test_no_profile_recompile"), @r" - fn test_no_profile_recompile@:4: + // On the final version, greet_final should be a Send fallback, not a SideExit. + assert_snapshot!(hir_string("test_final_version"), @r" + fn test_final_version@:4: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf @@ -15036,13 +15006,11 @@ mod hir_opt_tests { IfFalse v16, bb4(v9, v17) v19:Truthy = RefineType v10, Truthy v23:Fixnum[42] = Const Value(42) - PatchPoint MethodRedefined(Object@0x1008, greet_recompile@0x1010, cme:0x1018) - v43:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] - v44:BasicObject = SendDirect v43, 0x1040, :greet_recompile (0x1050), v23 + v25:BasicObject = Send v9, :greet_final, v23 # SendFallbackReason: SendWithoutBlock: no profile data available CheckInterrupts - Return v44 + Return v25 bb4(v30:BasicObject, v31:Falsy): - v35:StringExact[VALUE(0x1058)] = Const Value(VALUE(0x1058)) + v35:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v36:StringExact = StringCopy v35 CheckInterrupts Return v36 From d3144721e972461da823b8abb8f50ffa8e1c7e8a Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 27 Mar 2026 09:11:52 -0700 Subject: [PATCH 08/22] ZJIT: Look up final version status dynamically Instead of storing is_final_version as a field on Function, compute it dynamically in convert_no_profile_sends by checking the payload's version count against MAX_ISEQ_VERSIONS. --- zjit/src/hir.rs | 12 ++---------- zjit/src/hir/opt_tests.rs | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index b5c5fffaa709c0..9f3a5a9883b63d 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2372,10 +2372,6 @@ pub struct Function { /// Whether previously, a function for this ISEQ was invalidated due to /// singleton class creation (violation of NoSingletonClass invariant). was_invalidated_for_singleton_class_creation: bool, - /// Whether this is the last allowed version for this ISEQ (at MAX_ISEQ_VERSIONS). - /// When true, convert_no_profile_sends skips converting sends to SideExits since - /// no further recompilation is possible. - is_final_version: bool, /// The types for the parameters of this function. They are copied to the type /// of entry block params after infer_types() fills Empty to all insn_types. param_types: Vec, @@ -2483,7 +2479,6 @@ impl Function { Function { iseq, was_invalidated_for_singleton_class_creation: false, - is_final_version: false, insns: vec![], insn_types: vec![], union_find: UnionFind::new().into(), @@ -5039,7 +5034,8 @@ impl Function { // On the final version, recompilation is not possible, so converting sends to // SideExits would just add overhead (the exit fires every time without benefit). // Keep them as Send fallbacks so the interpreter handles them directly. - if self.is_final_version { + let payload = get_or_create_iseq_payload(self.iseq); + if payload.versions.len() + 1 >= crate::codegen::MAX_ISEQ_VERSIONS { return; } for block in self.rpo() { @@ -6827,10 +6823,6 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let mut profiles = ProfileOracle::new(payload); let mut fun = Function::new(iseq); fun.was_invalidated_for_singleton_class_creation = payload.was_invalidated_for_singleton_class_creation; - // invalidate_iseq_version only invalidates when versions.len() < MAX_ISEQ_VERSIONS. - // After this compilation, versions.len() will be current + 1. If that reaches the limit, - // exit profiling can't trigger another recompile, so SideExits would fire permanently. - fun.is_final_version = payload.versions.len() + 1 >= crate::codegen::MAX_ISEQ_VERSIONS; // Compute a map of PC->Block by finding jump targets let jit_entry_insns = jit_entry_insns(iseq); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 96882445edbeb6..15db98502582e0 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -14969,7 +14969,7 @@ mod hir_opt_tests { // // Use call_threshold=3 to ensure the method is auto-compiled before hir_string() builds // the HIR. The auto-compile creates version 1, and hir_string() creates version 2 - // (= MAX_ISEQ_VERSIONS), so is_final_version is true. + // (= MAX_ISEQ_VERSIONS), so this is the final version. set_call_threshold(3); eval(" def greet_final(x) = x.to_s From f8a9d28cd1ed42316cfd6d3980ca2101440f110a Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 27 Mar 2026 09:14:50 -0700 Subject: [PATCH 09/22] ZJIT: Restore test_recompile_no_profile_send test Keep the original SideExit recompile test alongside the new final-version test since they cover different behaviors. Remove the intermediate HIR snapshot since hir_string() now sees the auto-compiled version as final. --- zjit/src/hir/opt_tests.rs | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 15db98502582e0..3390ca3cbe8d3c 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -14961,6 +14961,66 @@ mod hir_opt_tests { "); } + #[test] + fn test_recompile_no_profile_send() { + // Test the SideExit → recompile flow: a no-profile send becomes a SideExit, + // the exit profiles the send, triggers recompilation, and the new version + // optimizes it to SendDirect. + eval(" + def greet_recompile(x) = x.to_s + def test_no_profile_recompile(flag) + if flag + greet_recompile(42) + else + 'hello' + end + end + "); + + // With call_threshold=2, num_profiles=1: + // 1st call profiles (flag=false, so greet is never reached) + // 2nd call compiles (greet has no profile data -> SideExit recompile) + eval("test_no_profile_recompile(false); test_no_profile_recompile(false)"); + + // Now call with flag=true. This hits the SideExit, which profiles + // the send and invalidates the ISEQ for recompilation. + eval("test_no_profile_recompile(true)"); + + // After profiling via the side exit, rebuilding HIR should now + // have a SendDirect for greet_recompile instead of SideExit. + assert_snapshot!(hir_string("test_no_profile_recompile"), @r" + fn test_no_profile_recompile@:4: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :flag@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :flag@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + CheckInterrupts + v16:CBool = Test v10 + v17:Falsy = RefineType v10, Falsy + IfFalse v16, bb4(v9, v17) + v19:Truthy = RefineType v10, Truthy + v23:Fixnum[42] = Const Value(42) + PatchPoint MethodRedefined(Object@0x1008, greet_recompile@0x1010, cme:0x1018) + v43:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] + v44:BasicObject = SendDirect v43, 0x1040, :greet_recompile (0x1050), v23 + CheckInterrupts + Return v44 + bb4(v30:BasicObject, v31:Falsy): + v35:StringExact[VALUE(0x1058)] = Const Value(VALUE(0x1058)) + v36:StringExact = StringCopy v35 + CheckInterrupts + Return v36 + "); + } + #[test] fn test_no_profile_send_on_final_version() { // On the final ISEQ version (MAX_ISEQ_VERSIONS reached), no-profile sends should From 2e753bf278b414b247d55928819fd8bd93ace62c Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Tue, 24 Mar 2026 01:40:51 -0700 Subject: [PATCH 10/22] Move rb_class_allocate_instance into gc.c --- gc.c | 31 +++++++++++++++++++++++++++++++ internal/object.h | 8 +++++++- object.c | 35 ----------------------------------- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/gc.c b/gc.c index a52ef98772a4b0..eb949bfae7c492 100644 --- a/gc.c +++ b/gc.c @@ -1073,6 +1073,37 @@ rb_wb_protected_newobj_of(rb_execution_context_t *ec, VALUE klass, VALUE flags, return newobj_of(rb_ec_ractor_ptr(ec), klass, flags, shape_id, TRUE, size); } +VALUE +rb_class_allocate_instance(VALUE klass) +{ + uint32_t index_tbl_num_entries = RCLASS_MAX_IV_COUNT(klass); + + size_t size = rb_obj_embedded_size(index_tbl_num_entries); + if (!rb_gc_size_allocatable_p(size)) { + size = sizeof(struct RObject); + } + + // There might be a NEWOBJ tracepoint callback, and it may set fields. + // So the shape must be passed to `NEWOBJ_OF`. + VALUE flags = T_OBJECT | (RGENGC_WB_PROTECTED_OBJECT ? FL_WB_PROTECTED : 0); + NEWOBJ_OF_WITH_SHAPE(o, struct RObject, klass, flags, rb_shape_root(rb_gc_heap_id_for_size(size)), size, 0); + VALUE obj = (VALUE)o; + +#if RUBY_DEBUG + RUBY_ASSERT(!rb_shape_obj_too_complex_p(obj)); + VALUE *ptr = ROBJECT_FIELDS(obj); + size_t fields_count = RSHAPE_LEN(RBASIC_SHAPE_ID(obj)); + for (size_t i = fields_count; i < ROBJECT_FIELDS_CAPACITY(obj); i++) { + ptr[i] = Qundef; + } + if (rb_obj_class(obj) != rb_class_real(klass)) { + rb_bug("Expected rb_class_allocate_instance to set the class correctly"); + } +#endif + + return obj; +} + void rb_gc_register_pinning_obj(VALUE obj) { diff --git a/internal/object.h b/internal/object.h index 3bde53c31b10c6..22da9ddb5e26fc 100644 --- a/internal/object.h +++ b/internal/object.h @@ -11,7 +11,7 @@ #include "ruby/ruby.h" /* for VALUE */ /* object.c */ -size_t rb_obj_embedded_size(uint32_t fields_count); + VALUE rb_class_allocate_instance(VALUE klass); VALUE rb_class_search_ancestor(VALUE klass, VALUE super); NORETURN(void rb_undefined_alloc(VALUE klass)); @@ -60,4 +60,10 @@ RBASIC_SET_CLASS(VALUE obj, VALUE klass) RBASIC_SET_CLASS_RAW(obj, klass); RB_OBJ_WRITTEN(obj, oldv, klass); } + +static inline size_t +rb_obj_embedded_size(uint32_t fields_count) +{ + return offsetof(struct RObject, as.ary) + (sizeof(VALUE) * fields_count); +} #endif /* INTERNAL_OBJECT_H */ diff --git a/object.c b/object.c index cb21741f315ec3..149b70b16a4a18 100644 --- a/object.c +++ b/object.c @@ -90,11 +90,6 @@ static ID id_instance_variables_to_inspect; /*! \endcond */ -size_t -rb_obj_embedded_size(uint32_t fields_count) -{ - return offsetof(struct RObject, as.ary) + (sizeof(VALUE) * fields_count); -} VALUE rb_obj_hide(VALUE obj) @@ -114,36 +109,6 @@ rb_obj_reveal(VALUE obj, VALUE klass) return obj; } -VALUE -rb_class_allocate_instance(VALUE klass) -{ - uint32_t index_tbl_num_entries = RCLASS_MAX_IV_COUNT(klass); - - size_t size = rb_obj_embedded_size(index_tbl_num_entries); - if (!rb_gc_size_allocatable_p(size)) { - size = sizeof(struct RObject); - } - - // There might be a NEWOBJ tracepoint callback, and it may set fields. - // So the shape must be passed to `NEWOBJ_OF`. - VALUE flags = T_OBJECT | (RGENGC_WB_PROTECTED_OBJECT ? FL_WB_PROTECTED : 0); - NEWOBJ_OF_WITH_SHAPE(o, struct RObject, klass, flags, rb_shape_root(rb_gc_heap_id_for_size(size)), size, 0); - VALUE obj = (VALUE)o; - -#if RUBY_DEBUG - RUBY_ASSERT(!rb_shape_obj_too_complex_p(obj)); - VALUE *ptr = ROBJECT_FIELDS(obj); - size_t fields_count = RSHAPE_LEN(RBASIC_SHAPE_ID(obj)); - for (size_t i = fields_count; i < ROBJECT_FIELDS_CAPACITY(obj); i++) { - ptr[i] = Qundef; - } - if (rb_obj_class(obj) != rb_class_real(klass)) { - rb_bug("Expected rb_class_allocate_instance to set the class correctly"); - } -#endif - - return obj; -} VALUE rb_obj_setup(VALUE obj, VALUE klass, VALUE type) From 8ea9f63e8470caa0f8d0d21c94fa75f8eec57b17 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 27 Mar 2026 17:10:29 +0100 Subject: [PATCH 11/22] Revert "Exclude rbs tests which need updates for {Method,UnboundMethod,Proc}#source_location" This reverts commit c161328240cd18f6e77b29fae61048fd9b7c730d. --- tool/rbs_skip_tests | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tool/rbs_skip_tests b/tool/rbs_skip_tests index 4bcb5707a51d9d..39ac16cb8f1551 100644 --- a/tool/rbs_skip_tests +++ b/tool/rbs_skip_tests @@ -47,11 +47,6 @@ test_compile(RegexpSingletonTest) test_linear_time?(RegexpSingletonTest) test_new(RegexpSingletonTest) -## Failed tests caused by unreleased version of Ruby -test_source_location(MethodInstanceTest) -test_source_location(ProcInstanceTest) -test_source_location(UnboundMethodInstanceTest) - # Errno::ENOENT: No such file or directory - bundle test_collection_install__pathname_set(RBS::CliTest) test_collection_install__set_pathname__manifest(RBS::CliTest) From 0ca6e5bbdbe16a39b2d288d2c2858a10a35265f1 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 27 Mar 2026 17:11:47 +0100 Subject: [PATCH 12/22] Revert "Update version guards in ruby/spec" This reverts commit 1d0cc4c5b376996f53ea74a8896ff10fae054252. --- spec/ruby/core/method/source_location_spec.rb | 4 ++-- spec/ruby/core/proc/source_location_spec.rb | 8 ++++---- spec/ruby/core/unboundmethod/source_location_spec.rb | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/ruby/core/method/source_location_spec.rb b/spec/ruby/core/method/source_location_spec.rb index 87413a2ab6780d..1b175ebabac044 100644 --- a/spec/ruby/core/method/source_location_spec.rb +++ b/spec/ruby/core/method/source_location_spec.rb @@ -109,10 +109,10 @@ def f eval('def self.m; end', nil, "foo", 100) end location = c.method(:m).source_location - ruby_version_is(""..."4.1") do + ruby_version_is(""..."4.0") do location.should == ["foo", 100] end - ruby_version_is("4.1") do + ruby_version_is("4.0") do location.should == ["foo", 100, 0, 100, 15] end end diff --git a/spec/ruby/core/proc/source_location_spec.rb b/spec/ruby/core/proc/source_location_spec.rb index fd33f21a26e8b3..18f1ca274c5488 100644 --- a/spec/ruby/core/proc/source_location_spec.rb +++ b/spec/ruby/core/proc/source_location_spec.rb @@ -53,12 +53,12 @@ end it "works even if the proc was created on the same line" do - ruby_version_is(""..."4.1") do + ruby_version_is(""..."4.0") do proc { true }.source_location.should == [__FILE__, __LINE__] Proc.new { true }.source_location.should == [__FILE__, __LINE__] -> { true }.source_location.should == [__FILE__, __LINE__] end - ruby_version_is("4.1") do + ruby_version_is("4.0") do proc { true }.source_location.should == [__FILE__, __LINE__, 11, __LINE__, 19] Proc.new { true }.source_location.should == [__FILE__, __LINE__, 15, __LINE__, 23] -> { true }.source_location.should == [__FILE__, __LINE__, 6, __LINE__, 17] @@ -94,10 +94,10 @@ it "works for eval with a given line" do proc = eval('-> {}', nil, "foo", 100) location = proc.source_location - ruby_version_is(""..."4.1") do + ruby_version_is(""..."4.0") do location.should == ["foo", 100] end - ruby_version_is("4.1") do + ruby_version_is("4.0") do location.should == ["foo", 100, 0, 100, 5] end end diff --git a/spec/ruby/core/unboundmethod/source_location_spec.rb b/spec/ruby/core/unboundmethod/source_location_spec.rb index 9cc219801738d7..85078ff34e8cd5 100644 --- a/spec/ruby/core/unboundmethod/source_location_spec.rb +++ b/spec/ruby/core/unboundmethod/source_location_spec.rb @@ -55,10 +55,10 @@ eval('def m; end', nil, "foo", 100) end location = c.instance_method(:m).source_location - ruby_version_is(""..."4.1") do + ruby_version_is(""..."4.0") do location.should == ["foo", 100] end - ruby_version_is("4.1") do + ruby_version_is("4.0") do location.should == ["foo", 100, 0, 100, 10] end end From 9f25970e50ba5a2d702597a38b5e8e271d282148 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 27 Mar 2026 17:12:09 +0100 Subject: [PATCH 13/22] Revert "[Bug #21783] Fix documentation of {Method,UnboundMethod,Proc}#source_location" This reverts commit 31c3bcb4818d9f62a6ad5c284f152abb40d552d0. --- proc.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proc.c b/proc.c index f1fc9780607341..09b3847d2d91c8 100644 --- a/proc.c +++ b/proc.c @@ -1545,9 +1545,9 @@ rb_iseq_location(const rb_iseq_t *iseq) * The returned Array contains: * (1) the Ruby source filename * (2) the line number where the definition starts - * (3) the position where the definition starts, in number of bytes from the start of the line + * (3) the column number where the definition starts * (4) the line number where the definition ends - * (5) the position where the definitions ends, in number of bytes from the start of the line + * (5) the column number where the definitions ends * * This method will return +nil+ if the Proc was not defined in Ruby (i.e. native). */ @@ -3206,9 +3206,9 @@ rb_method_entry_location(const rb_method_entry_t *me) * The returned Array contains: * (1) the Ruby source filename * (2) the line number where the definition starts - * (3) the position where the definition starts, in number of bytes from the start of the line + * (3) the column number where the definition starts * (4) the line number where the definition ends - * (5) the position where the definitions ends, in number of bytes from the start of the line + * (5) the column number where the definitions ends * * This method will return +nil+ if the method was not defined in Ruby (i.e. native). */ From 096213b2f152c71a8141e450225530c3fa1163a7 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 27 Mar 2026 17:15:48 +0100 Subject: [PATCH 14/22] Revert "Reapply "[Feature #6012] Extend `source_location` for end position" This reverts commit 8f5e0d8ff82ff63d60da445826fa44be3d8d0820. --- NEWS.md | 7 --- proc.c | 16 ++---- spec/ruby/core/method/source_location_spec.rb | 16 ++---- spec/ruby/core/proc/source_location_spec.rb | 51 +++++++------------ .../unboundmethod/source_location_spec.rb | 18 +++---- test/ruby/test_lambda.rb | 12 ++--- test/ruby/test_proc.rb | 18 +++---- 7 files changed, 48 insertions(+), 90 deletions(-) diff --git a/NEWS.md b/NEWS.md index 7e00b3a851c0a9..34ebad7d01b697 100644 --- a/NEWS.md +++ b/NEWS.md @@ -33,12 +33,6 @@ Note: We're only listing outstanding class updates. * Method - * `Method#source_location`, `Proc#source_location`, and - `UnboundMethod#source_location` now return extended location - information with 5 elements: `[path, start_line, start_column, - end_line, end_column]`. The previous 2-element format `[path, - line]` can still be obtained by calling `.take(2)` on the result. - [[Feature #6012]] * `Array#pack` accepts a new format `R` and `r` for unpacking unsigned and signed LEB128 encoded integers. [[Feature #21785]] @@ -145,7 +139,6 @@ A lot of work has gone into making Ractors more stable, performant, and usable. ## JIT -[Feature #6012]: https://bugs.ruby-lang.org/issues/6012 [Feature #8948]: https://bugs.ruby-lang.org/issues/8948 [Feature #15330]: https://bugs.ruby-lang.org/issues/15330 [Feature #21390]: https://bugs.ruby-lang.org/issues/21390 diff --git a/proc.c b/proc.c index 09b3847d2d91c8..35b1f3b60862cf 100644 --- a/proc.c +++ b/proc.c @@ -1515,20 +1515,14 @@ proc_eq(VALUE self, VALUE other) static VALUE iseq_location(const rb_iseq_t *iseq) { - VALUE loc[5]; - int i = 0; + VALUE loc[2]; if (!iseq) return Qnil; rb_iseq_check(iseq); - loc[i++] = rb_iseq_path(iseq); - const rb_code_location_t *cl = &ISEQ_BODY(iseq)->location.code_location; - loc[i++] = RB_INT2NUM(cl->beg_pos.lineno); - loc[i++] = RB_INT2NUM(cl->beg_pos.column); - loc[i++] = RB_INT2NUM(cl->end_pos.lineno); - loc[i++] = RB_INT2NUM(cl->end_pos.column); - RUBY_ASSERT_ALWAYS(i == numberof(loc)); - - return rb_ary_new_from_values(i, loc); + loc[0] = rb_iseq_path(iseq); + loc[1] = RB_INT2NUM(ISEQ_BODY(iseq)->location.first_lineno); + + return rb_ary_new4(2, loc); } VALUE diff --git a/spec/ruby/core/method/source_location_spec.rb b/spec/ruby/core/method/source_location_spec.rb index 1b175ebabac044..c5b296f6e2a7b0 100644 --- a/spec/ruby/core/method/source_location_spec.rb +++ b/spec/ruby/core/method/source_location_spec.rb @@ -11,23 +11,23 @@ end it "sets the first value to the path of the file in which the method was defined" do - file = @method.source_location[0] + file = @method.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/classes.rb', __dir__) end it "sets the last value to an Integer representing the line on which the method was defined" do - line = @method.source_location[1] + line = @method.source_location.last line.should be_an_instance_of(Integer) line.should == 5 end it "returns the last place the method was defined" do - MethodSpecs::SourceLocation.method(:redefined).source_location[1].should == 13 + MethodSpecs::SourceLocation.method(:redefined).source_location.last.should == 13 end it "returns the location of the original method even if it was aliased" do - MethodSpecs::SourceLocation.new.method(:aka).source_location[1].should == 17 + MethodSpecs::SourceLocation.new.method(:aka).source_location.last.should == 17 end it "works for methods defined with a block" do @@ -108,13 +108,7 @@ def f c = Class.new do eval('def self.m; end', nil, "foo", 100) end - location = c.method(:m).source_location - ruby_version_is(""..."4.0") do - location.should == ["foo", 100] - end - ruby_version_is("4.0") do - location.should == ["foo", 100, 0, 100, 15] - end + c.method(:m).source_location.should == ["foo", 100] end describe "for a Method generated by respond_to_missing?" do diff --git a/spec/ruby/core/proc/source_location_spec.rb b/spec/ruby/core/proc/source_location_spec.rb index 18f1ca274c5488..a8b99287d5c380 100644 --- a/spec/ruby/core/proc/source_location_spec.rb +++ b/spec/ruby/core/proc/source_location_spec.rb @@ -17,64 +17,57 @@ end it "sets the first value to the path of the file in which the proc was defined" do - file = @proc.source_location[0] + file = @proc.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) - file = @proc_new.source_location[0] + file = @proc_new.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) - file = @lambda.source_location[0] + file = @lambda.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) - file = @method.source_location[0] + file = @method.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/source_location.rb', __dir__) end - it "sets the second value to an Integer representing the line on which the proc was defined" do - line = @proc.source_location[1] + it "sets the last value to an Integer representing the line on which the proc was defined" do + line = @proc.source_location.last line.should be_an_instance_of(Integer) line.should == 4 - line = @proc_new.source_location[1] + line = @proc_new.source_location.last line.should be_an_instance_of(Integer) line.should == 12 - line = @lambda.source_location[1] + line = @lambda.source_location.last line.should be_an_instance_of(Integer) line.should == 8 - line = @method.source_location[1] + line = @method.source_location.last line.should be_an_instance_of(Integer) line.should == 15 end it "works even if the proc was created on the same line" do - ruby_version_is(""..."4.0") do - proc { true }.source_location.should == [__FILE__, __LINE__] - Proc.new { true }.source_location.should == [__FILE__, __LINE__] - -> { true }.source_location.should == [__FILE__, __LINE__] - end - ruby_version_is("4.0") do - proc { true }.source_location.should == [__FILE__, __LINE__, 11, __LINE__, 19] - Proc.new { true }.source_location.should == [__FILE__, __LINE__, 15, __LINE__, 23] - -> { true }.source_location.should == [__FILE__, __LINE__, 6, __LINE__, 17] - end + proc { true }.source_location.should == [__FILE__, __LINE__] + Proc.new { true }.source_location.should == [__FILE__, __LINE__] + -> { true }.source_location.should == [__FILE__, __LINE__] end it "returns the first line of a multi-line proc (i.e. the line containing 'proc do')" do - ProcSpecs::SourceLocation.my_multiline_proc.source_location[1].should == 20 - ProcSpecs::SourceLocation.my_multiline_proc_new.source_location[1].should == 34 - ProcSpecs::SourceLocation.my_multiline_lambda.source_location[1].should == 27 + ProcSpecs::SourceLocation.my_multiline_proc.source_location.last.should == 20 + ProcSpecs::SourceLocation.my_multiline_proc_new.source_location.last.should == 34 + ProcSpecs::SourceLocation.my_multiline_lambda.source_location.last.should == 27 end it "returns the location of the proc's body; not necessarily the proc itself" do - ProcSpecs::SourceLocation.my_detached_proc.source_location[1].should == 41 - ProcSpecs::SourceLocation.my_detached_proc_new.source_location[1].should == 51 - ProcSpecs::SourceLocation.my_detached_lambda.source_location[1].should == 46 + ProcSpecs::SourceLocation.my_detached_proc.source_location.last.should == 41 + ProcSpecs::SourceLocation.my_detached_proc_new.source_location.last.should == 51 + ProcSpecs::SourceLocation.my_detached_lambda.source_location.last.should == 46 end it "returns the same value for a proc-ified method as the method reports" do @@ -93,12 +86,6 @@ it "works for eval with a given line" do proc = eval('-> {}', nil, "foo", 100) - location = proc.source_location - ruby_version_is(""..."4.0") do - location.should == ["foo", 100] - end - ruby_version_is("4.0") do - location.should == ["foo", 100, 0, 100, 5] - end + proc.source_location.should == ["foo", 100] end end diff --git a/spec/ruby/core/unboundmethod/source_location_spec.rb b/spec/ruby/core/unboundmethod/source_location_spec.rb index 85078ff34e8cd5..5c2f14362c40b4 100644 --- a/spec/ruby/core/unboundmethod/source_location_spec.rb +++ b/spec/ruby/core/unboundmethod/source_location_spec.rb @@ -7,23 +7,23 @@ end it "sets the first value to the path of the file in which the method was defined" do - file = @method.source_location[0] + file = @method.source_location.first file.should be_an_instance_of(String) file.should == File.realpath('fixtures/classes.rb', __dir__) end - it "sets the second value to an Integer representing the line on which the method was defined" do - line = @method.source_location[1] + it "sets the last value to an Integer representing the line on which the method was defined" do + line = @method.source_location.last line.should be_an_instance_of(Integer) line.should == 5 end it "returns the last place the method was defined" do - UnboundMethodSpecs::SourceLocation.method(:redefined).unbind.source_location[1].should == 13 + UnboundMethodSpecs::SourceLocation.method(:redefined).unbind.source_location.last.should == 13 end it "returns the location of the original method even if it was aliased" do - UnboundMethodSpecs::SourceLocation.instance_method(:aka).source_location[1].should == 17 + UnboundMethodSpecs::SourceLocation.instance_method(:aka).source_location.last.should == 17 end it "works for define_method methods" do @@ -54,12 +54,6 @@ c = Class.new do eval('def m; end', nil, "foo", 100) end - location = c.instance_method(:m).source_location - ruby_version_is(""..."4.0") do - location.should == ["foo", 100] - end - ruby_version_is("4.0") do - location.should == ["foo", 100, 0, 100, 10] - end + c.instance_method(:m).source_location.should == ["foo", 100] end end diff --git a/test/ruby/test_lambda.rb b/test/ruby/test_lambda.rb index c1858a36ddecdd..ce0f3387600397 100644 --- a/test/ruby/test_lambda.rb +++ b/test/ruby/test_lambda.rb @@ -276,27 +276,27 @@ def test_break end def test_do_lambda_source_location - exp = [__LINE__ + 1, 10, __LINE__ + 5, 7] + exp_lineno = __LINE__ + 3 lmd = ->(x, y, z) do # end - file, *loc = lmd.source_location + file, lineno = lmd.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp, loc) + assert_equal(exp_lineno, lineno, "must be at the beginning of the block") end def test_brace_lambda_source_location - exp = [__LINE__ + 1, 10, __LINE__ + 5, 5] + exp_lineno = __LINE__ + 3 lmd = ->(x, y, z) { # } - file, *loc = lmd.source_location + file, lineno = lmd.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp, loc) + assert_equal(exp_lineno, lineno, "must be at the beginning of the block") end def test_not_orphan_return diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb index 959ea87f25d667..f74342322f5b78 100644 --- a/test/ruby/test_proc.rb +++ b/test/ruby/test_proc.rb @@ -513,7 +513,7 @@ def test_binding_source_location file, lineno = method(:source_location_test).to_proc.binding.source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(@@line_of_source_location_test[0], lineno, 'Bug #2427') + assert_equal(@@line_of_source_location_test, lineno, 'Bug #2427') end def test_binding_error_unless_ruby_frame @@ -1499,19 +1499,15 @@ def test_to_s assert_include(EnvUtil.labeled_class(name, Proc).new {}.to_s, name) end - @@line_of_source_location_test = [__LINE__ + 1, 2, __LINE__ + 3, 5] + @@line_of_source_location_test = __LINE__ + 1 def source_location_test a=1, b=2 end def test_source_location - file, *loc = method(:source_location_test).source_location + file, lineno = method(:source_location_test).source_location assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(@@line_of_source_location_test, loc, 'Bug #2427') - - file, *loc = self.class.instance_method(:source_location_test).source_location - assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(@@line_of_source_location_test, loc, 'Bug #2427') + assert_equal(@@line_of_source_location_test, lineno, 'Bug #2427') end @@line_of_attr_reader_source_location_test = __LINE__ + 3 @@ -1544,13 +1540,13 @@ def block_source_location_test(*args, &block) end def test_block_source_location - exp_loc = [__LINE__ + 3, 49, __LINE__ + 4, 49] - file, *loc = block_source_location_test(1, + exp_lineno = __LINE__ + 3 + file, lineno = block_source_location_test(1, 2, 3) do end assert_match(/^#{ Regexp.quote(__FILE__) }$/, file) - assert_equal(exp_loc, loc) + assert_equal(exp_lineno, lineno) end def test_splat_without_respond_to From 0e4c2a380fab2475226df11e58ac3b0d0e8f9d59 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 27 Mar 2026 17:16:27 +0100 Subject: [PATCH 15/22] Revert "[DOC] Describe new return value of source_location" This reverts commit edff523407ac54b09a95a946c927656262595178. --- proc.c | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/proc.c b/proc.c index 35b1f3b60862cf..99fb880881b9d8 100644 --- a/proc.c +++ b/proc.c @@ -1533,17 +1533,10 @@ rb_iseq_location(const rb_iseq_t *iseq) /* * call-seq: - * prc.source_location -> [String, Integer, Integer, Integer, Integer] + * prc.source_location -> [String, Integer] * - * Returns the location where the Proc was defined. - * The returned Array contains: - * (1) the Ruby source filename - * (2) the line number where the definition starts - * (3) the column number where the definition starts - * (4) the line number where the definition ends - * (5) the column number where the definitions ends - * - * This method will return +nil+ if the Proc was not defined in Ruby (i.e. native). + * Returns the Ruby source filename and line number containing this proc + * or +nil+ if this proc was not defined in Ruby (i.e. native). */ VALUE @@ -3194,17 +3187,10 @@ rb_method_entry_location(const rb_method_entry_t *me) /* * call-seq: - * meth.source_location -> [String, Integer, Integer, Integer, Integer] - * - * Returns the location where the method was defined. - * The returned Array contains: - * (1) the Ruby source filename - * (2) the line number where the definition starts - * (3) the column number where the definition starts - * (4) the line number where the definition ends - * (5) the column number where the definitions ends + * meth.source_location -> [String, Integer] * - * This method will return +nil+ if the method was not defined in Ruby (i.e. native). + * Returns the Ruby source filename and line number containing this method + * or nil if this method was not defined in Ruby (i.e. native). */ VALUE From 6035121c933c965cebf93f624066c95dabbc6d1d Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 27 Mar 2026 17:17:36 +0100 Subject: [PATCH 16/22] Fix NEWS entry about Array#pack --- NEWS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index 34ebad7d01b697..4a6710e836c778 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,6 +11,11 @@ Note that each entry is kept to a minimum, see links for details. Note: We're only listing outstanding class updates. +* Array + + * `Array#pack` accepts a new format `R` and `r` for unpacking unsigned + and signed LEB128 encoded integers. [[Feature #21785]] + * ENV * `ENV.fetch_values` is added. It returns an array of values for the @@ -31,11 +36,6 @@ Note: We're only listing outstanding class updates. * `MatchData#integer_at` is added. It converts the matched substring to integer and return the result. [[Feature #21932]] -* Method - - * `Array#pack` accepts a new format `R` and `r` for unpacking unsigned - and signed LEB128 encoded integers. [[Feature #21785]] - * Regexp * All instances of `Regexp` are now frozen, not just literals. From 400727a27116203830722fdbf319d84d86277930 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 27 Mar 2026 14:56:50 +0100 Subject: [PATCH 17/22] [ruby/prism] Remove unused variable in tests https://github.com/ruby/prism/commit/5bb64a246d --- test/prism/ruby/find_fixtures.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/prism/ruby/find_fixtures.rb b/test/prism/ruby/find_fixtures.rb index df82cc700408b1..c1bef0d0e65ff0 100644 --- a/test/prism/ruby/find_fixtures.rb +++ b/test/prism/ruby/find_fixtures.rb @@ -45,7 +45,6 @@ module DefineMethod end module ForLoop - items = [1, 2, 3] for_proc = nil o = Object.new def o.each(&block) = block.call(block) From 93c6f97bf29093ffbbbb2d523e7aaf320c6bf7fc Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 27 Mar 2026 14:52:12 +0100 Subject: [PATCH 18/22] [ruby/prism] Fix -Wconversion warnings from gcc 8.5.0 https://github.com/ruby/prism/commit/6069d67d22 --- prism/prism.c | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/prism/prism.c b/prism/prism.c index 0e798fdce88305..72c49da6f29499 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -13888,7 +13888,7 @@ parse_arguments(pm_parser_t *parser, pm_arguments_t *arguments, bool accepts_for PRISM_FALLTHROUGH default: { if (argument == NULL) { - argument = parse_value_expression(parser, PM_BINDING_POWER_DEFINED, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (!parsed_first_argument ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0u) | PM_PARSE_ACCEPTS_LABEL, PM_ERR_EXPECT_ARGUMENT, (uint16_t) (depth + 1)); + argument = parse_value_expression(parser, PM_BINDING_POWER_DEFINED, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (!parsed_first_argument ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0u) | PM_PARSE_ACCEPTS_LABEL), PM_ERR_EXPECT_ARGUMENT, (uint16_t) (depth + 1)); } bool contains_keywords = false; @@ -17792,7 +17792,7 @@ parse_case(pm_parser_t *parser, uint8_t flags, uint16_t depth) { } else if (!token_begins_expression_p(parser->current.type)) { predicate = NULL; } else { - predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CASE_EXPRESSION_AFTER_CASE, (uint16_t) (depth + 1)); + predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CASE_EXPRESSION_AFTER_CASE, (uint16_t) (depth + 1)); while (accept2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON)); } @@ -17911,11 +17911,11 @@ parse_case(pm_parser_t *parser, uint8_t flags, uint16_t depth) { * statements. */ if (accept1(parser, PM_TOKEN_KEYWORD_IF_MODIFIER)) { pm_token_t keyword = parser->previous; - pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_IF_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_IF_PREDICATE, (uint16_t) (depth + 1)); pattern = UP(pm_if_node_modifier_create(parser, pattern, &keyword, predicate)); } else if (accept1(parser, PM_TOKEN_KEYWORD_UNLESS_MODIFIER)) { pm_token_t keyword = parser->previous; - pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_UNLESS_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_UNLESS_PREDICATE, (uint16_t) (depth + 1)); pattern = UP(pm_unless_node_modifier_create(parser, pattern, &keyword, predicate)); } @@ -18004,7 +18004,7 @@ parse_class(pm_parser_t *parser, uint8_t flags, uint16_t depth) { if (accept1(parser, PM_TOKEN_LESS_LESS)) { pm_token_t operator = parser->previous; - pm_node_t *expression = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_EXPECT_EXPRESSION_AFTER_LESS_LESS, (uint16_t) (depth + 1)); + pm_node_t *expression = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_EXPECT_EXPRESSION_AFTER_LESS_LESS, (uint16_t) (depth + 1)); pm_parser_scope_push(parser, true); if (!match2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON)) { @@ -18053,7 +18053,7 @@ parse_class(pm_parser_t *parser, uint8_t flags, uint16_t depth) { parser->command_start = true; parser_lex(parser); - superclass = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CLASS_SUPERCLASS, (uint16_t) (depth + 1)); + superclass = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CLASS_SUPERCLASS, (uint16_t) (depth + 1)); } else { superclass = NULL; } @@ -18235,7 +18235,7 @@ parse_def(pm_parser_t *parser, pm_binding_power_t binding_power, uint8_t flags, parser_lex(parser); pm_token_t lparen = parser->previous; - pm_node_t *expression = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_DEF_RECEIVER, (uint16_t) (depth + 1)); + pm_node_t *expression = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_DEF_RECEIVER, (uint16_t) (depth + 1)); accept1(parser, PM_TOKEN_NEWLINE); expect1(parser, PM_TOKEN_PARENTHESIS_RIGHT, PM_ERR_EXPECT_RPAREN); @@ -19121,7 +19121,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u pm_static_literals_free(&hash_keys); parsed_bare_hash = true; } else { - element = parse_value_expression(parser, PM_BINDING_POWER_DEFINED, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_LABEL, PM_ERR_ARRAY_EXPRESSION, (uint16_t) (depth + 1)); + element = parse_value_expression(parser, PM_BINDING_POWER_DEFINED, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_LABEL), PM_ERR_ARRAY_EXPRESSION, (uint16_t) (depth + 1)); if (pm_symbol_node_label_p(parser, element) || accept1(parser, PM_TOKEN_EQUAL_GREATER)) { if (parsed_bare_hash) { @@ -19851,7 +19851,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u expect1(parser, PM_TOKEN_KEYWORD_IN, PM_ERR_FOR_IN); pm_token_t in_keyword = parser->previous; - pm_node_t *collection = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_FOR_COLLECTION, (uint16_t) (depth + 1)); + pm_node_t *collection = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_FOR_COLLECTION, (uint16_t) (depth + 1)); pm_do_loop_stack_pop(parser); pm_token_t do_keyword = { 0 }; @@ -19954,7 +19954,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u } } } else { - receiver = parse_expression(parser, PM_BINDING_POWER_NOT, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_NOT_EXPRESSION, (uint16_t) (depth + 1)); + receiver = parse_expression(parser, PM_BINDING_POWER_NOT, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_NOT_EXPRESSION, (uint16_t) (depth + 1)); } return UP(pm_call_node_not_create(parser, receiver, &message, &arguments)); @@ -20000,7 +20000,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u parser_lex(parser); pm_token_t keyword = parser->previous; - pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_UNTIL_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_UNTIL_PREDICATE, (uint16_t) (depth + 1)); pm_do_loop_stack_pop(parser); context_pop(parser); @@ -20033,7 +20033,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u parser_lex(parser); pm_token_t keyword = parser->previous; - pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_WHILE_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, PM_BINDING_POWER_COMPOSITION, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_WHILE_PREDICATE, (uint16_t) (depth + 1)); pm_do_loop_stack_pop(parser); context_pop(parser); @@ -20367,7 +20367,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u parser_lex(parser); pm_token_t operator = parser->previous; - pm_node_t *receiver = parse_expression(parser, pm_binding_powers[parser->previous.type].right, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (binding_power < PM_BINDING_POWER_MATCH ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0), PM_ERR_UNARY_RECEIVER, (uint16_t) (depth + 1)); + pm_node_t *receiver = parse_expression(parser, pm_binding_powers[parser->previous.type].right, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (binding_power < PM_BINDING_POWER_MATCH ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), PM_ERR_UNARY_RECEIVER, (uint16_t) (depth + 1)); pm_call_node_t *node = pm_call_node_unary_create(parser, &operator, receiver, "!"); pm_conditional_predicate(parser, receiver, PM_CONDITIONAL_PREDICATE_TYPE_NOT); @@ -20568,7 +20568,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, u */ static pm_node_t * parse_assignment_value(pm_parser_t *parser, pm_binding_power_t previous_binding_power, pm_binding_power_t binding_power, uint8_t flags, pm_diagnostic_id_t diag_id, uint16_t depth) { - pm_node_t *value = parse_value_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (previous_binding_power == PM_BINDING_POWER_ASSIGNMENT ? (flags & PM_PARSE_ACCEPTS_COMMAND_CALL) : (previous_binding_power < PM_BINDING_POWER_MATCH ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), diag_id, (uint16_t) (depth + 1)); + pm_node_t *value = parse_value_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (previous_binding_power == PM_BINDING_POWER_ASSIGNMENT ? (flags & PM_PARSE_ACCEPTS_COMMAND_CALL) : (previous_binding_power < PM_BINDING_POWER_MATCH ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0))), diag_id, (uint16_t) (depth + 1)); // Assignments whose value is a command call (e.g., a = b c) can only // be followed by modifiers (if/unless/while/until/rescue) and not by @@ -20650,7 +20650,7 @@ parse_assignment_values(pm_parser_t *parser, pm_binding_power_t previous_binding bool permitted = true; if (previous_binding_power != PM_BINDING_POWER_STATEMENT && match1(parser, PM_TOKEN_USTAR)) permitted = false; - pm_node_t *value = parse_starred_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (previous_binding_power == PM_BINDING_POWER_ASSIGNMENT ? (flags & PM_PARSE_ACCEPTS_COMMAND_CALL) : (previous_binding_power < PM_BINDING_POWER_MODIFIER ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), diag_id, (uint16_t) (depth + 1)); + pm_node_t *value = parse_starred_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (previous_binding_power == PM_BINDING_POWER_ASSIGNMENT ? (flags & PM_PARSE_ACCEPTS_COMMAND_CALL) : (previous_binding_power < PM_BINDING_POWER_MODIFIER ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0))), diag_id, (uint16_t) (depth + 1)); if (!permitted) pm_parser_err_node(parser, value, PM_ERR_UNEXPECTED_MULTI_WRITE); parse_assignment_value_local(parser, value); @@ -20705,7 +20705,7 @@ parse_assignment_values(pm_parser_t *parser, pm_binding_power_t previous_binding } } - pm_node_t *right = parse_expression(parser, pm_binding_powers[PM_TOKEN_KEYWORD_RESCUE_MODIFIER].right, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (accepts_command_call_inner ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0), PM_ERR_RESCUE_MODIFIER_VALUE, (uint16_t) (depth + 1)); + pm_node_t *right = parse_expression(parser, pm_binding_powers[PM_TOKEN_KEYWORD_RESCUE_MODIFIER].right, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (accepts_command_call_inner ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), PM_ERR_RESCUE_MODIFIER_VALUE, (uint16_t) (depth + 1)); context_pop(parser); return UP(pm_rescue_modifier_node_create(parser, value, &rescue, right)); @@ -21417,14 +21417,14 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t case PM_TOKEN_KEYWORD_AND: { parser_lex(parser); - pm_node_t *right = parse_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (parser->previous.type == PM_TOKEN_KEYWORD_AND ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0), PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR, (uint16_t) (depth + 1)); + pm_node_t *right = parse_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (parser->previous.type == PM_TOKEN_KEYWORD_AND ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR, (uint16_t) (depth + 1)); return UP(pm_and_node_create(parser, node, &token, right)); } case PM_TOKEN_KEYWORD_OR: case PM_TOKEN_PIPE_PIPE: { parser_lex(parser); - pm_node_t *right = parse_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (parser->previous.type == PM_TOKEN_KEYWORD_OR ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0), PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR, (uint16_t) (depth + 1)); + pm_node_t *right = parse_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | (parser->previous.type == PM_TOKEN_KEYWORD_OR ? PM_PARSE_ACCEPTS_COMMAND_CALL : 0)), PM_ERR_EXPECT_EXPRESSION_AFTER_OPERATOR, (uint16_t) (depth + 1)); return UP(pm_or_node_create(parser, node, &token, right)); } case PM_TOKEN_EQUAL_TILDE: { @@ -21653,14 +21653,14 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t pm_token_t keyword = parser->current; parser_lex(parser); - pm_node_t *predicate = parse_value_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_IF_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_IF_PREDICATE, (uint16_t) (depth + 1)); return UP(pm_if_node_modifier_create(parser, node, &keyword, predicate)); } case PM_TOKEN_KEYWORD_UNLESS_MODIFIER: { pm_token_t keyword = parser->current; parser_lex(parser); - pm_node_t *predicate = parse_value_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_UNLESS_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_UNLESS_PREDICATE, (uint16_t) (depth + 1)); return UP(pm_unless_node_modifier_create(parser, node, &keyword, predicate)); } case PM_TOKEN_KEYWORD_UNTIL_MODIFIER: { @@ -21668,7 +21668,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t pm_statements_node_t *statements = pm_statements_node_create(parser); pm_statements_node_body_append(parser, statements, node, true); - pm_node_t *predicate = parse_value_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_UNTIL_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_UNTIL_PREDICATE, (uint16_t) (depth + 1)); return UP(pm_until_node_modifier_create(parser, &token, predicate, statements, PM_NODE_TYPE_P(node, PM_BEGIN_NODE) ? PM_LOOP_FLAGS_BEGIN_MODIFIER : 0)); } case PM_TOKEN_KEYWORD_WHILE_MODIFIER: { @@ -21676,7 +21676,7 @@ parse_expression_infix(pm_parser_t *parser, pm_node_t *node, pm_binding_power_t pm_statements_node_t *statements = pm_statements_node_create(parser); pm_statements_node_body_append(parser, statements, node, true); - pm_node_t *predicate = parse_value_expression(parser, binding_power, (flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL, PM_ERR_CONDITIONAL_WHILE_PREDICATE, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_value_expression(parser, binding_power, (uint8_t) ((flags & PM_PARSE_ACCEPTS_DO_BLOCK) | PM_PARSE_ACCEPTS_COMMAND_CALL), PM_ERR_CONDITIONAL_WHILE_PREDICATE, (uint16_t) (depth + 1)); return UP(pm_while_node_modifier_create(parser, &token, predicate, statements, PM_NODE_TYPE_P(node, PM_BEGIN_NODE) ? PM_LOOP_FLAGS_BEGIN_MODIFIER : 0)); } case PM_TOKEN_QUESTION_MARK: { From ec2b2b77248ff5ba44e8d644adb74c6ceb3a803d Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 26 Mar 2026 20:12:56 -0700 Subject: [PATCH 19/22] ZJIT: Replace blockiseq: Option with block: Option Introduce a BlockHandler enum to represent how a block is passed to a send-like instruction. For now it has only the BlockIseq(IseqPtr) variant, making this a pure mechanical rename with no behavior change. --- zjit/src/codegen.rs | 28 +++++++------- zjit/src/hir.rs | 93 +++++++++++++++++++++++++-------------------- 2 files changed, 65 insertions(+), 56 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 3a6d60af2df471..db0477f7c98abb 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -21,7 +21,7 @@ use crate::stats::{counter_ptr, with_time_stat, Counter, Counter::{compile_time_ use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; use crate::backend::lir::{self, Assembler, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NATIVE_STACK_PTR, Opnd, SP, SideExit, SideExitRecompile, Target, asm_ccall, asm_comment}; use crate::hir::{iseq_to_hir, BlockId, Invariant, RangeType, SideExitReason::{self, *}, SpecialBackrefSymbol, SpecialObjectType}; -use crate::hir::{Const, FrameState, Function, Insn, InsnId, SendFallbackReason}; +use crate::hir::{BlockHandler, Const, FrameState, Function, Insn, InsnId, SendFallbackReason}; use crate::hir_type::{types, Type}; use crate::options::{get_option, PerfMap}; use crate::cast::IntoUsize; @@ -615,10 +615,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::Param => unreachable!("block.insns should not have Insn::Param"), Insn::LoadArg { .. } => return Ok(()), // compiled in the LoadArg pre-pass above Insn::Snapshot { .. } => return Ok(()), // we don't need to do anything for this instruction at the moment - &Insn::Send { cd, blockiseq: None, state, reason, .. } => gen_send_without_block(jit, asm, cd, &function.frame_state(state), reason), - &Insn::Send { cd, blockiseq: Some(blockiseq), state, reason, .. } => gen_send(jit, asm, cd, blockiseq, &function.frame_state(state), reason), + &Insn::Send { cd, block: None, state, reason, .. } => gen_send_without_block(jit, asm, cd, &function.frame_state(state), reason), + &Insn::Send { cd, block: Some(BlockHandler::BlockIseq(blockiseq)), state, reason, .. } => gen_send(jit, asm, cd, blockiseq, &function.frame_state(state), reason), &Insn::SendForward { cd, blockiseq, state, reason, .. } => gen_send_forward(jit, asm, cd, blockiseq, &function.frame_state(state), reason), - Insn::SendDirect { cme, iseq, recv, args, kw_bits, blockiseq, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), *kw_bits, &function.frame_state(*state), *blockiseq), + Insn::SendDirect { cme, iseq, recv, args, kw_bits, block, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), *kw_bits, &function.frame_state(*state), *block), &Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason), &Insn::InvokeSuperForward { cd, blockiseq, state, reason, .. } => gen_invokesuperforward(jit, asm, cd, blockiseq, &function.frame_state(state), reason), &Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason), @@ -680,10 +680,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio // There's no test case for this because no core cfuncs have this many parameters. But C extensions could have such methods. Insn::CCallWithFrame { cd, state, args, .. } if args.len() + 1 > C_ARG_OPNDS.len() => gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::CCallWithFrameTooManyArgs), - Insn::CCallWithFrame { cfunc, recv, name, args, cme, state, blockiseq, .. } => - gen_ccall_with_frame(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, *blockiseq, &function.frame_state(*state)), - Insn::CCallVariadic { cfunc, recv, name, args, cme, state, blockiseq, return_type: _, elidable: _ } => { - gen_ccall_variadic(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, *blockiseq, &function.frame_state(*state)) + Insn::CCallWithFrame { cfunc, recv, name, args, cme, state, block, .. } => + gen_ccall_with_frame(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, *block, &function.frame_state(*state)), + Insn::CCallVariadic { cfunc, recv, name, args, cme, state, block, return_type: _, elidable: _ } => { + gen_ccall_variadic(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, *block, &function.frame_state(*state)) } Insn::GetIvar { self_val, id, ic, state: _ } => gen_getivar(jit, asm, opnd!(self_val), *id, *ic), Insn::SetGlobal { id, val, state } => no_output!(gen_setglobal(jit, asm, *id, opnd!(val), &function.frame_state(*state))), @@ -1010,7 +1010,7 @@ fn gen_ccall_with_frame( recv: Opnd, args: Vec, cme: *const rb_callable_method_entry_t, - blockiseq: Option, + block: Option, state: &FrameState, ) -> lir::Opnd { gen_incr_counter(asm, Counter::non_variadic_cfunc_optimized_send_count); @@ -1026,7 +1026,7 @@ fn gen_ccall_with_frame( gen_spill_stack(jit, asm, state); gen_spill_locals(jit, asm, state); - let block_handler_specval = if let Some(block_iseq) = blockiseq { + let block_handler_specval = if let Some(BlockHandler::BlockIseq(block_iseq)) = block { // Change cfp->block_code in the current frame. See vm_caller_setup_arg_block(). // VM_CFP_TO_CAPTURED_BLOCK then turns &cfp->self into a block handler. // rb_captured_block->code.iseq aliases with cfp->block_code. @@ -1101,7 +1101,7 @@ fn gen_ccall_variadic( recv: Opnd, args: Vec, cme: *const rb_callable_method_entry_t, - blockiseq: Option, + block: Option, state: &FrameState, ) -> lir::Opnd { gen_incr_counter(asm, Counter::variadic_cfunc_optimized_send_count); @@ -1120,7 +1120,7 @@ fn gen_ccall_variadic( gen_spill_stack(jit, asm, state); gen_spill_locals(jit, asm, state); - let block_handler_specval = if let Some(blockiseq) = blockiseq { + let block_handler_specval = if let Some(BlockHandler::BlockIseq(blockiseq)) = block { gen_block_handler_specval(asm, blockiseq) } else { VM_BLOCK_HANDLER_NONE.into() @@ -1542,7 +1542,7 @@ fn gen_send_iseq_direct( args: Vec, kw_bits: u32, state: &FrameState, - blockiseq: Option, + block: Option, ) -> lir::Opnd { gen_incr_counter(asm, Counter::iseq_optimized_send_count); @@ -1562,7 +1562,7 @@ fn gen_send_iseq_direct( // The HIR specialization guards ensure we will only reach here for literal blocks, // not &block forwarding, &:foo, etc. Thise are rejected in `type_specialize` by // `unspecializable_call_type`. - let block_handler = blockiseq.map(|b| gen_block_handler_specval(asm, b)); + let block_handler = block.map(|BlockHandler::BlockIseq(b)| gen_block_handler_specval(asm, b)); let callee_is_bmethod = VM_METHOD_TYPE_BMETHOD == unsafe { get_cme_def_type(cme) }; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 9f3a5a9883b63d..2abdc693cd0c6e 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -750,6 +750,14 @@ impl Display for SendFallbackReason { } } +/// How a block is passed to a send-like instruction. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum BlockHandler { + /// Literal block ISEQ passed to the send (can be null for the `send` YARV + /// instruction when no literal block is present). + BlockIseq(IseqPtr), +} + /// An instruction in the SSA IR. The output of an instruction is referred to by the index of /// the instruction ([`InsnId`]). SSA form enables this, and [`UnionFind`] ([`Function::find`]) /// helps with editing. @@ -931,7 +939,7 @@ pub enum Insn { state: InsnId, return_type: Type, elidable: bool, - blockiseq: Option, + block: Option, }, /// Call a variadic C function with signature: func(int argc, VALUE *argv, VALUE recv) @@ -945,7 +953,7 @@ pub enum Insn { state: InsnId, return_type: Type, elidable: bool, - blockiseq: Option, + block: Option, }, /// Un-optimized fallback implementation (dynamic dispatch) for send-ish instructions @@ -953,7 +961,7 @@ pub enum Insn { Send { recv: InsnId, cd: *const rb_call_data, - blockiseq: Option, + block: Option, args: Vec, state: InsnId, reason: SendFallbackReason, @@ -1004,7 +1012,7 @@ pub enum Insn { iseq: IseqPtr, args: Vec, kw_bits: u32, - blockiseq: Option, + block: Option, state: InsnId, }, @@ -1865,18 +1873,19 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::Jump(target) => { write!(f, "Jump {target}") } Insn::IfTrue { val, target } => { write!(f, "IfTrue {val}, {target}") } Insn::IfFalse { val, target } => { write!(f, "IfFalse {val}, {target}") } - Insn::SendDirect { recv, cd, iseq, args, blockiseq, .. } => { - write!(f, "SendDirect {recv}, {:p}, :{} ({:?})", self.ptr_map.map_ptr(blockiseq), ruby_call_method_name(*cd), self.ptr_map.map_ptr(iseq))?; + Insn::SendDirect { recv, cd, iseq, args, block, .. } => { + let blockiseq = block.map(|BlockHandler::BlockIseq(iseq)| iseq); + write!(f, "SendDirect {recv}, {:p}, :{} ({:?})", self.ptr_map.map_ptr(&blockiseq), ruby_call_method_name(*cd), self.ptr_map.map_ptr(iseq))?; for arg in args { write!(f, ", {arg}")?; } Ok(()) } - Insn::Send { recv, cd, args, blockiseq, reason, .. } => { + Insn::Send { recv, cd, args, block, reason, .. } => { // For tests, we want to check HIR snippets textually. Addresses change // between runs, making tests fail. Instead, pick an arbitrary hex value to // use as a "pointer" so we can check the rest of the HIR. - if let Some(blockiseq) = *blockiseq { + if let Some(BlockHandler::BlockIseq(blockiseq)) = *block { write!(f, "Send {recv}, {:p}, :{}", self.ptr_map.map_ptr(blockiseq), ruby_call_method_name(*cd))?; } else { write!(f, "Send {recv}, :{}", ruby_call_method_name(*cd))?; @@ -1991,12 +2000,12 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { } Ok(()) }, - Insn::CCallWithFrame { cfunc, recv, args, name, cme, blockiseq, .. } => { + Insn::CCallWithFrame { cfunc, recv, args, name, cme, block, .. } => { write!(f, "CCallWithFrame {recv}, :{}@{:p}", qualified_method_name(unsafe { (**cme).owner }, *name), self.ptr_map.map_ptr(cfunc))?; for arg in args { write!(f, ", {arg}")?; } - if let Some(blockiseq) = blockiseq { + if let Some(BlockHandler::BlockIseq(blockiseq)) = block { write!(f, ", block={:p}", self.ptr_map.map_ptr(blockiseq))?; } Ok(()) @@ -2755,20 +2764,20 @@ impl Function { str: find!(str), state, }, - &SendDirect { recv, cd, cme, iseq, ref args, kw_bits, blockiseq, state } => SendDirect { + &SendDirect { recv, cd, cme, iseq, ref args, kw_bits, block, state } => SendDirect { recv: find!(recv), cd, cme, iseq, args: find_vec!(args), kw_bits, - blockiseq, + block, state, }, - &Send { recv, cd, blockiseq, ref args, state, reason } => Send { + &Send { recv, cd, block, ref args, state, reason } => Send { recv: find!(recv), cd, - blockiseq, + block, args: find_vec!(args), state, reason, @@ -2817,7 +2826,7 @@ impl Function { &ObjectAlloc { val, state } => ObjectAlloc { val: find!(val), state }, &ObjectAllocClass { class, state } => ObjectAllocClass { class, state: find!(state) }, &CCall { cfunc, recv, ref args, name, owner, return_type, elidable } => CCall { cfunc, recv: find!(recv), args: find_vec!(args), name, owner, return_type, elidable }, - &CCallWithFrame { cd, cfunc, recv, ref args, cme, name, state, return_type, elidable, blockiseq } => CCallWithFrame { + &CCallWithFrame { cd, cfunc, recv, ref args, cme, name, state, return_type, elidable, block } => CCallWithFrame { cd, cfunc, recv: find!(recv), @@ -2827,10 +2836,10 @@ impl Function { state: find!(state), return_type, elidable, - blockiseq, + block, }, - &CCallVariadic { cfunc, recv, ref args, cme, name, state, return_type, elidable, blockiseq } => CCallVariadic { - cfunc, recv: find!(recv), args: find_vec!(args), cme, name, state, return_type, elidable, blockiseq + &CCallVariadic { cfunc, recv, ref args, cme, name, state, return_type, elidable, block } => CCallVariadic { + cfunc, recv: find!(recv), args: find_vec!(args), cme, name, state, return_type, elidable, block }, &CheckMatch { target, pattern, flag, state } => CheckMatch { target: find!(target), pattern: find!(pattern), flag, state: find!(state) }, &Defined { op_type, obj, pushval, v, state } => Defined { op_type, obj, pushval, v: find!(v), state: find!(state) }, @@ -3566,12 +3575,12 @@ impl Function { assert!(self.blocks[block.0].insns.is_empty()); for insn_id in old_insns { match self.find(insn_id) { - Insn::Send { recv, blockiseq: None, args, state, cd, .. } if ruby_call_method_id(cd) == ID!(freeze) && args.is_empty() => + Insn::Send { recv, block: None, args, state, cd, .. } if ruby_call_method_id(cd) == ID!(freeze) && args.is_empty() => self.try_rewrite_freeze(block, insn_id, recv, state), - Insn::Send { recv, blockiseq: None, args, state, cd, .. } if ruby_call_method_id(cd) == ID!(minusat) && args.is_empty() => + Insn::Send { recv, block: None, args, state, cd, .. } if ruby_call_method_id(cd) == ID!(minusat) && args.is_empty() => self.try_rewrite_uminus(block, insn_id, recv, state), - Insn::Send { mut recv, cd, state, blockiseq, args, .. } => { - let has_block = blockiseq.is_some(); + Insn::Send { mut recv, cd, state, block: send_block, args, .. } => { + let has_block = send_block.is_some(); let frame_state = self.frame_state(state); let (klass, profiled_type) = match self.resolve_receiver_type(recv, self.type_of(recv), frame_state.insn_idx) { ReceiverTypeResolution::StaticallyKnown { class } => (class, None), @@ -3670,7 +3679,7 @@ impl Function { recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); } - let send_direct = self.push_insn(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, blockiseq }); + let send_direct = self.push_insn(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, block: send_block }); self.make_equal_to(insn_id, send_direct); } else if !has_block && def_type == VM_METHOD_TYPE_BMETHOD { let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) }; @@ -3712,7 +3721,7 @@ impl Function { recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); } - let send_direct = self.push_insn(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, blockiseq: None }); + let send_direct = self.push_insn(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, block: None }); self.make_equal_to(insn_id, send_direct); } else if !has_block && def_type == VM_METHOD_TYPE_IVAR && args.is_empty() { // Check if we're accessing ivars of a Class or Module object as they require single-ractor mode. @@ -3877,7 +3886,7 @@ impl Function { self.make_equal_to(insn_id, guard); } else { let recv = self.push_insn(block, Insn::GuardType { val, guard_type: Type::from_profiled_type(recv_type), state}); - let send_to_s = self.push_insn(block, Insn::Send { recv, cd, blockiseq: None, args: vec![], state, reason: ObjToStringNotString }); + let send_to_s = self.push_insn(block, Insn::Send { recv, cd, block: None, args: vec![], state, reason: ObjToStringNotString }); self.make_equal_to(insn_id, send_to_s); } } @@ -4075,7 +4084,7 @@ impl Function { args: processed_args, kw_bits, state: send_state, - blockiseq: None, + block: None, }); self.make_equal_to(insn_id, send_direct); @@ -4142,7 +4151,7 @@ impl Function { state, return_type: types::BasicObject, elidable: false, - blockiseq: None, + block: None, }) }; self.make_equal_to(insn_id, ccall); @@ -4192,7 +4201,7 @@ impl Function { state, return_type: types::BasicObject, elidable: false, - blockiseq: None, + block: None, }) }; self.make_equal_to(insn_id, ccall); @@ -4588,7 +4597,7 @@ impl Function { send: Insn, send_insn_id: InsnId, ) -> Result<(), ()> { - let Insn::Send { mut recv, cd, blockiseq, args, state, .. } = send else { + let Insn::Send { mut recv, cd, block: send_block, args, state, .. } = send else { return Err(()); }; @@ -4646,10 +4655,10 @@ impl Function { return Err(()); } - let blockiseq = match blockiseq { + let blockiseq = match send_block { None => unreachable!("went to reduce_send_without_block_to_ccall"), - Some(p) if p.is_null() => None, - Some(blockiseq) => Some(blockiseq), + Some(BlockHandler::BlockIseq(p)) if p.is_null() => None, + Some(BlockHandler::BlockIseq(blockiseq)) => Some(blockiseq), }; let cfunc = unsafe { get_cme_def_body_cfunc(cme) }; @@ -4695,7 +4704,7 @@ impl Function { state, return_type: types::BasicObject, elidable: false, - blockiseq, + block: blockiseq.map(BlockHandler::BlockIseq), }); fun.make_equal_to(send_insn_id, ccall); Ok(()) @@ -4732,7 +4741,7 @@ impl Function { state, return_type: types::BasicObject, elidable: false, - blockiseq + block: blockiseq.map(BlockHandler::BlockIseq), }); fun.make_equal_to(send_insn_id, ccall); @@ -4886,7 +4895,7 @@ impl Function { state, return_type, elidable, - blockiseq: None, + block: None, }); fun.make_equal_to(send_insn_id, ccall); } @@ -4960,7 +4969,7 @@ impl Function { state, return_type, elidable, - blockiseq: None, + block: None, }); fun.make_equal_to(send_insn_id, ccall); @@ -4986,7 +4995,7 @@ impl Function { for insn_id in old_insns { let send = self.find(insn_id); match send { - send @ Insn::Send { recv, blockiseq: None, .. } => { + send @ Insn::Send { recv, block: None, .. } => { let recv_type = self.type_of(recv); if reduce_send_without_block_to_ccall(self, block, recv_type, send, insn_id).is_ok() { continue; @@ -7694,7 +7703,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } let args = state.stack_pop_n(argc as usize)?; let recv = state.stack_pop()?; - let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq: None, args, state: exit_id, reason: Uncategorized(opcode) }); + let send = fun.push_insn(block, Insn::Send { recv, cd, block: None, args, state: exit_id, reason: Uncategorized(opcode) }); state.stack_push(send); } YARVINSN_opt_hash_freeze => { @@ -7852,7 +7861,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let recv = state.stack_pop().unwrap(); let refined_recv = fun.push_insn(block, Insn::RefineType { val: recv, new_type }); state.replace(recv, refined_recv); - let send = fun.push_insn(block, Insn::Send { recv: refined_recv, cd, blockiseq: None, args, state: snapshot, reason: Uncategorized(opcode) }); + let send = fun.push_insn(block, Insn::Send { recv: refined_recv, cd, block: None, args, state: snapshot, reason: Uncategorized(opcode) }); state.stack_push(send); fun.push_insn(block, Insn::Jump(BranchEdge { target: join_block, args: state.as_args(self_param) })); block @@ -7891,7 +7900,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let args = state.stack_pop_n(argc as usize)?; let recv = state.stack_pop()?; let reason = SendWithoutBlockPolymorphicFallback; - let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq: None, args, state: exit_id, reason }); + let send = fun.push_insn(block, Insn::Send { recv, cd, block: None, args, state: exit_id, reason }); state.stack_push(send); fun.push_insn(block, Insn::Jump(BranchEdge { target: join_block, args: state.as_args(self_param) })); break; // End the block @@ -7900,7 +7909,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let args = state.stack_pop_n(argc as usize)?; let recv = state.stack_pop()?; - let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq: None, args, state: exit_id, reason: Uncategorized(opcode) }); + let send = fun.push_insn(block, Insn::Send { recv, cd, block: None, args, state: exit_id, reason: Uncategorized(opcode) }); state.stack_push(send); } YARVINSN_send => { @@ -7923,7 +7932,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; let recv = state.stack_pop()?; - let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq: Some(blockiseq), args, state: exit_id, reason: Uncategorized(opcode) }); + let send = fun.push_insn(block, Insn::Send { recv, cd, block: Some(BlockHandler::BlockIseq(blockiseq)), args, state: exit_id, reason: Uncategorized(opcode) }); state.stack_push(send); if !blockiseq.is_null() { From 48b7e45cddd4a4caac7a46ed2282e35aa4b12649 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 26 Mar 2026 20:19:56 -0700 Subject: [PATCH 20/22] ZJIT: Add BlockHandler::BlockArg for &proc sends Add a BlockArg variant to BlockHandler for sends that pass a block via &proc (ARGS_BLOCKARG). Previously these were represented as Some(BlockIseq(null)), requiring a normalization hack in reduce_send_to_ccall. Now the three cases are explicit: None (no block), Some(BlockIseq) (literal block), Some(BlockArg) (&proc). This also fixes a latent bug: with specialized_instruction disabled (as power_assert does), regular calls use `send` instead of opt_send_without_block. A null blockiseq was misinterpreted as having a block, causing gen_block_handler_specval to write a captured block handler with a null code pointer. --- zjit/src/codegen.rs | 3 ++- zjit/src/hir.rs | 40 ++++++++++++++++++++++++++------------- zjit/src/hir/opt_tests.rs | 17 +++++++++-------- zjit/src/hir/tests.rs | 8 ++++---- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index db0477f7c98abb..9c4a456af91d2f 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -617,6 +617,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::Snapshot { .. } => return Ok(()), // we don't need to do anything for this instruction at the moment &Insn::Send { cd, block: None, state, reason, .. } => gen_send_without_block(jit, asm, cd, &function.frame_state(state), reason), &Insn::Send { cd, block: Some(BlockHandler::BlockIseq(blockiseq)), state, reason, .. } => gen_send(jit, asm, cd, blockiseq, &function.frame_state(state), reason), + &Insn::Send { cd, block: Some(BlockHandler::BlockArg), state, reason, .. } => gen_send(jit, asm, cd, std::ptr::null(), &function.frame_state(state), reason), &Insn::SendForward { cd, blockiseq, state, reason, .. } => gen_send_forward(jit, asm, cd, blockiseq, &function.frame_state(state), reason), Insn::SendDirect { cme, iseq, recv, args, kw_bits, block, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), *kw_bits, &function.frame_state(*state), *block), &Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason), @@ -1562,7 +1563,7 @@ fn gen_send_iseq_direct( // The HIR specialization guards ensure we will only reach here for literal blocks, // not &block forwarding, &:foo, etc. Thise are rejected in `type_specialize` by // `unspecializable_call_type`. - let block_handler = block.map(|BlockHandler::BlockIseq(b)| gen_block_handler_specval(asm, b)); + let block_handler = block.map(|bh| match bh { BlockHandler::BlockIseq(b) => gen_block_handler_specval(asm, b), BlockHandler::BlockArg => unreachable!("BlockArg in gen_send_iseq_direct") }); let callee_is_bmethod = VM_METHOD_TYPE_BMETHOD == unsafe { get_cme_def_type(cme) }; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 2abdc693cd0c6e..69f145e8c3d049 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -753,9 +753,10 @@ impl Display for SendFallbackReason { /// How a block is passed to a send-like instruction. #[derive(Clone, Copy, Debug, PartialEq)] pub enum BlockHandler { - /// Literal block ISEQ passed to the send (can be null for the `send` YARV - /// instruction when no literal block is present). + /// Literal block ISEQ (e.g. `foo { ... }`) BlockIseq(IseqPtr), + /// Block arg passed via &proc (e.g. `foo(&block)`) + BlockArg, } /// An instruction in the SSA IR. The output of an instruction is referred to by the index of @@ -1874,7 +1875,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::IfTrue { val, target } => { write!(f, "IfTrue {val}, {target}") } Insn::IfFalse { val, target } => { write!(f, "IfFalse {val}, {target}") } Insn::SendDirect { recv, cd, iseq, args, block, .. } => { - let blockiseq = block.map(|BlockHandler::BlockIseq(iseq)| iseq); + let blockiseq = block.map(|bh| match bh { BlockHandler::BlockIseq(iseq) => iseq, BlockHandler::BlockArg => unreachable!() }); write!(f, "SendDirect {recv}, {:p}, :{} ({:?})", self.ptr_map.map_ptr(&blockiseq), ruby_call_method_name(*cd), self.ptr_map.map_ptr(iseq))?; for arg in args { write!(f, ", {arg}")?; @@ -1885,10 +1886,13 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { // For tests, we want to check HIR snippets textually. Addresses change // between runs, making tests fail. Instead, pick an arbitrary hex value to // use as a "pointer" so we can check the rest of the HIR. - if let Some(BlockHandler::BlockIseq(blockiseq)) = *block { - write!(f, "Send {recv}, {:p}, :{}", self.ptr_map.map_ptr(blockiseq), ruby_call_method_name(*cd))?; - } else { - write!(f, "Send {recv}, :{}", ruby_call_method_name(*cd))?; + match *block { + Some(BlockHandler::BlockIseq(blockiseq)) => + write!(f, "Send {recv}, {:p}, :{}", self.ptr_map.map_ptr(blockiseq), ruby_call_method_name(*cd))?, + Some(BlockHandler::BlockArg) => + write!(f, "Send {recv}, &block, :{}", ruby_call_method_name(*cd))?, + None => + write!(f, "Send {recv}, :{}", ruby_call_method_name(*cd))?, } for arg in args { write!(f, ", {arg}")?; @@ -2005,8 +2009,12 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { for arg in args { write!(f, ", {arg}")?; } - if let Some(BlockHandler::BlockIseq(blockiseq)) = block { - write!(f, ", block={:p}", self.ptr_map.map_ptr(blockiseq))?; + match block { + Some(BlockHandler::BlockIseq(blockiseq)) => + write!(f, ", block={:p}", self.ptr_map.map_ptr(blockiseq))?, + Some(BlockHandler::BlockArg) => + write!(f, ", block=&block")?, + None => {} } Ok(()) }, @@ -4656,8 +4664,7 @@ impl Function { } let blockiseq = match send_block { - None => unreachable!("went to reduce_send_without_block_to_ccall"), - Some(BlockHandler::BlockIseq(p)) if p.is_null() => None, + None | Some(BlockHandler::BlockArg) => unreachable!("went to reduce_send_without_block_to_ccall"), Some(BlockHandler::BlockIseq(blockiseq)) => Some(blockiseq), }; @@ -7932,10 +7939,17 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; let recv = state.stack_pop()?; - let send = fun.push_insn(block, Insn::Send { recv, cd, block: Some(BlockHandler::BlockIseq(blockiseq)), args, state: exit_id, reason: Uncategorized(opcode) }); + let block_handler = if !blockiseq.is_null() { + Some(BlockHandler::BlockIseq(blockiseq)) + } else if block_arg { + Some(BlockHandler::BlockArg) + } else { + None + }; + let send = fun.push_insn(block, Insn::Send { recv, cd, block: block_handler, args, state: exit_id, reason: Uncategorized(opcode) }); state.stack_push(send); - if !blockiseq.is_null() { + if let Some(BlockHandler::BlockIseq(_)) = block_handler { // Reload locals that may have been modified by the blockiseq. // TODO: Avoid reloading locals that are not referenced by the blockiseq // or not used after this. Max thinks we could eventually DCE them. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 3390ca3cbe8d3c..0b7453c4ae4243 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -1485,9 +1485,10 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, length@0x1010, cme:0x1018) v24:ArrayExact = GuardType v10, ArrayExact - v25:BasicObject = CCallWithFrame v24, :Array#length@0x1040 + v25:CInt64 = ArrayLength v24 + v26:Fixnum = BoxFixnum v25 CheckInterrupts - Return v25 + Return v26 "); } @@ -4740,7 +4741,7 @@ mod hir_opt_tests { v18:CInt64 = LoadField v15, :_env_data_index_specval@0x1002 v19:CInt64 = GuardAnyBitSet v18, CUInt64(1) v20:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - v22:BasicObject = Send v9, 0x1001, :tap, v20 # SendFallbackReason: Send: no profile data available + v22:BasicObject = Send v9, &block, :tap, v20 # SendFallbackReason: Send: no profile data available CheckInterrupts Return v22 "); @@ -7916,7 +7917,7 @@ mod hir_opt_tests { 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, 0x1001, :map, v21 # SendFallbackReason: Complex argument passing + v23:BasicObject = Send v14, &block, :map, v21 # SendFallbackReason: Complex argument passing CheckInterrupts Return v23 "); @@ -7949,7 +7950,7 @@ mod hir_opt_tests { 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, 0x1001, :map, v21 # SendFallbackReason: Complex argument passing + v23:BasicObject = Send v14, &block, :map, v21 # SendFallbackReason: Complex argument passing CheckInterrupts Return v23 "); @@ -7983,7 +7984,7 @@ mod hir_opt_tests { 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, 0x1000, :map, v17 # SendFallbackReason: Complex argument passing + v19:BasicObject = Send v10, &block, :map, v17 # SendFallbackReason: Complex argument passing CheckInterrupts Return v19 "); @@ -11427,7 +11428,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v11:StaticSymbol[:the_block] = Const Value(VALUE(0x1000)) - v13:BasicObject = Send v6, 0x1008, :callee, v11 # SendFallbackReason: Complex argument passing + v13:BasicObject = Send v6, &block, :callee, v11 # SendFallbackReason: Complex argument passing CheckInterrupts Return v13 "); @@ -11465,7 +11466,7 @@ mod hir_opt_tests { 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, 0x1001, :map, v21 # SendFallbackReason: Complex argument passing + v23:BasicObject = Send v14, &block, :map, v21 # SendFallbackReason: Complex argument passing CheckInterrupts Return v23 "); diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index a26f699c5be74f..606df902f20183 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2095,7 +2095,7 @@ pub mod hir_build_tests { v7:BasicObject = LoadArg :a@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v16:BasicObject = Send v9, 0x1008, :foo, v10 # SendFallbackReason: Uncategorized(send) + v16:BasicObject = Send v9, &block, :foo, v10 # SendFallbackReason: Uncategorized(send) CheckInterrupts Return v16 "); @@ -3525,7 +3525,7 @@ pub mod hir_build_tests { v38:CInt64[0] = GuardBitEquals v37, CInt64(0) v39:NilClass = Const Value(nil) v41:NilClass = GuardType v20, NilClass - v43:BasicObject = Send v17, 0x1004, :foo, v18, v29, v41, v39 # SendFallbackReason: Uncategorized(send) + v43:BasicObject = Send v17, &block, :foo, v18, v29, v41, v39 # SendFallbackReason: Uncategorized(send) CheckInterrupts Return v43 "); @@ -3562,7 +3562,7 @@ pub mod hir_build_tests { v23:CInt64 = GuardAnyBitSet v22, CUInt64(1) v24:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) v26:HashExact = GuardType v12, HashExact - v28:BasicObject = Send v11, 0x1002, :foo, v26, v24 # SendFallbackReason: Uncategorized(send) + v28:BasicObject = Send v11, &block, :foo, v26, v24 # SendFallbackReason: Uncategorized(send) CheckInterrupts Return v28 "); @@ -3599,7 +3599,7 @@ pub mod hir_build_tests { v23:CInt64 = GuardAnyBitSet v22, CUInt64(1) v24:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) v26:HashExact = GuardType v12, HashExact - v28:BasicObject = Send v11, 0x1002, :foo, v26, v24 # SendFallbackReason: Uncategorized(send) + v28:BasicObject = Send v11, &block, :foo, v26, v24 # SendFallbackReason: Uncategorized(send) CheckInterrupts Return v28 "); From 75ac1dd608bbe468df8bdd713bb7e0f1eb64b593 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 26 Mar 2026 20:21:50 -0700 Subject: [PATCH 21/22] ZJIT: Recompile sends with literal blocks via exit profiling Extend convert_no_profile_sends to handle SendNoProfiles (sends with literal blocks), not just SendWithoutBlockNoProfiles. The match guards against BlockArg sends since &proc forwarding can't be exit-profiled. --- test/ruby/test_zjit.rb | 17 +++++++++++++++++ zjit/src/hir.rs | 2 +- zjit/src/hir/opt_tests.rs | 4 +--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 20fc5bce16a886..0c7d76bdf67292 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -399,6 +399,23 @@ def array.itself = :not_itself RUBY end + def test_send_no_profiles_with_disabled_specialized_instruction + # Regression test: when specialized_instruction is disabled (as power_assert does), + # eval'd code uses `send` instead of `opt_send_without_block`, producing SendNoProfiles. + # The `times` call with a literal block is the SendNoProfiles send whose exit profiling + # triggers recompilation of `run`. After recompilation, `make`'s eval("proc { }") crashes + # in vm_make_env_each because the caller frame's EP[-1] (specval) has a stale value. + assert_runs ':ok', <<~RUBY + RubyVM::InstructionSequence.compile_option = { specialized_instruction: false } + eval <<~'INNERRUBY' + def make = eval("proc { }") + def run(n) = n.times { make } + INNERRUBY + run(6) + :ok + RUBY + end + private # Assert that every method call in `test_script` can be compiled by ZJIT diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 69f145e8c3d049..7107f7046afbb9 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5059,7 +5059,7 @@ impl Function { assert!(self.blocks[block.0].insns.is_empty()); for insn_id in old_insns { match self.find(insn_id) { - Insn::Send { cd, state, reason: SendFallbackReason::SendWithoutBlockNoProfiles, .. } => { + Insn::Send { cd, state, reason: SendFallbackReason::SendWithoutBlockNoProfiles | SendFallbackReason::SendNoProfiles, .. } => { let argc = unsafe { vm_ci_argc((*cd).ci) } as i32; self.push_insn(block, Insn::SideExit { state, reason: SideExitReason::NoProfileSend, recompile: Some(argc) }); // SideExit is a terminator; don't add remaining instructions diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 0b7453c4ae4243..3cf02934e7ad23 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -4741,9 +4741,7 @@ mod hir_opt_tests { v18:CInt64 = LoadField v15, :_env_data_index_specval@0x1002 v19:CInt64 = GuardAnyBitSet v18, CUInt64(1) v20:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) - v22:BasicObject = Send v9, &block, :tap, v20 # SendFallbackReason: Send: no profile data available - CheckInterrupts - Return v22 + SideExit NoProfileSend recompile "); } From ec2ff4fc48ba2e1287365d862216d4abf6d483b3 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Fri, 27 Mar 2026 09:54:13 -0700 Subject: [PATCH 22/22] Remove unnecessary begin/end in do/end block in _tmpdir.rb --- tool/lib/_tmpdir.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tool/lib/_tmpdir.rb b/tool/lib/_tmpdir.rb index 89fd2b24cd6ae5..ac5b9be792ec63 100644 --- a/tool/lib/_tmpdir.rb +++ b/tool/lib/_tmpdir.rb @@ -85,16 +85,14 @@ def list_tree(parent, indent = "", &block) warn colorize.notice("Children under ")+colorize.fail(tmpdir)+":" Dir.chdir(tmpdir) do ls.list_tree(".") do |path, st| - begin - if st.directory? - Dir.rmdir(path) - else - File.unlink(path) - end - rescue Errno::EACCES - # On Windows, a killed process may still hold file locks briefly. - # Ignore and let FileUtils.rm_rf handle it below. + if st.directory? + Dir.rmdir(path) + else + File.unlink(path) end + rescue Errno::EACCES + # On Windows, a killed process may still hold file locks briefly. + # Ignore and let FileUtils.rm_rf handle it below. end end end