From 7896d376fb9cb602e2f5b26436c2ed8412b1ab9b Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Wed, 1 Jul 2026 18:50:54 +0000 Subject: [PATCH 01/11] Prohibit at_exit and END{} in non-main Ractors Registering an exit handler with Kernel#at_exit or END{} in a non-main Ractor behaved confusingly: the block ran in the main Ractor at process exit even though it was registered from another Ractor. Raise Ractor::IsolationError instead. [Feature #22139] Co-Authored-By: Claude Opus 4.8 (1M context) --- NEWS.md | 5 +++++ depend | 2 ++ eval_jump.c | 2 ++ test/ruby/test_ractor.rb | 22 ++++++++++++++++++++++ vm.c | 2 ++ 5 files changed, 33 insertions(+) diff --git a/NEWS.md b/NEWS.md index 0d725703b9c9f9..13839ed6076907 100644 --- a/NEWS.md +++ b/NEWS.md @@ -150,6 +150,10 @@ Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for det ## Compatibility issues +* `Kernel#at_exit` and `END {}` now raise `Ractor::IsolationError` when called + in a non-main Ractor. Previously the registered handler ran in the main + Ractor at process exit, which was confusing. [[Feature #22139]] + ## Stdlib compatibility issues ## C API updates @@ -207,6 +211,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #21861]: https://bugs.ruby-lang.org/issues/21861 [Feature #21932]: https://bugs.ruby-lang.org/issues/21932 [Feature #21981]: https://bugs.ruby-lang.org/issues/21981 +[Feature #22139]: https://bugs.ruby-lang.org/issues/22139 [PR #17201]: https://github.com/ruby/ruby/pull/17201 [RubyGems-v4.0.4]: https://github.com/rubygems/rubygems/releases/tag/v4.0.4 [RubyGems-v4.0.5]: https://github.com/rubygems/rubygems/releases/tag/v4.0.5 diff --git a/depend b/depend index bfde95272b0d95..96d0485d527d51 100644 --- a/depend +++ b/depend @@ -5452,6 +5452,7 @@ eval.$(OBJEXT): $(top_srcdir)/internal/imemo.h eval.$(OBJEXT): $(top_srcdir)/internal/inits.h eval.$(OBJEXT): $(top_srcdir)/internal/io.h eval.$(OBJEXT): $(top_srcdir)/internal/object.h +eval.$(OBJEXT): $(top_srcdir)/internal/ractor.h eval.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h eval.$(OBJEXT): $(top_srcdir)/internal/serial.h eval.$(OBJEXT): $(top_srcdir)/internal/set_table.h @@ -19742,6 +19743,7 @@ vm.$(OBJEXT): $(top_srcdir)/internal/numeric.h vm.$(OBJEXT): $(top_srcdir)/internal/object.h vm.$(OBJEXT): $(top_srcdir)/internal/parse.h vm.$(OBJEXT): $(top_srcdir)/internal/proc.h +vm.$(OBJEXT): $(top_srcdir)/internal/ractor.h vm.$(OBJEXT): $(top_srcdir)/internal/random.h vm.$(OBJEXT): $(top_srcdir)/internal/rational.h vm.$(OBJEXT): $(top_srcdir)/internal/re.h diff --git a/eval_jump.c b/eval_jump.c index 6ee8ff4a6f5f9a..7737148644ce37 100644 --- a/eval_jump.c +++ b/eval_jump.c @@ -4,6 +4,7 @@ */ #include "eval_intern.h" +#include "internal/ractor.h" /* exit */ @@ -42,6 +43,7 @@ rb_f_at_exit(VALUE _) if (!rb_block_given_p()) { rb_raise(rb_eArgError, "called without a block"); } + rb_ractor_ensure_main_ractor("can not call at_exit from non-main Ractors"); proc = rb_block_proc(); rb_set_end_proc(rb_call_end_proc, proc); return proc; diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index 65c4756dc0bc02..9c4bcad7f00742 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -240,6 +240,28 @@ def test_fork_raise_isolation_error RUBY end if Process.respond_to?(:fork) + def test_at_exit_raise_isolation_error + assert_ractor(<<~'RUBY') + ractor = Ractor.new do + at_exit { } + rescue Ractor::IsolationError => e + e + end + assert_equal Ractor::IsolationError, ractor.value.class + RUBY + end + + def test_END_raise_isolation_error + assert_ractor(<<~'RUBY', ignore_stderr: true) + ractor = Ractor.new do + END { nil } + rescue Ractor::IsolationError => e + e + end + assert_equal Ractor::IsolationError, ractor.value.class + RUBY + end + def test_require_raises_and_no_ractor_belonging_issue assert_ractor(<<~'RUBY') require "tempfile" diff --git a/vm.c b/vm.c index 86c0e3391553a6..d0f944592ae55a 100644 --- a/vm.c +++ b/vm.c @@ -24,6 +24,7 @@ #include "internal/missing.h" #include "internal/object.h" #include "internal/proc.h" +#include "internal/ractor.h" #include "internal/re.h" #include "internal/ruby_parser.h" #include "internal/st.h" @@ -4115,6 +4116,7 @@ m_core_undef_method(VALUE self, VALUE cbase, VALUE sym) static VALUE m_core_set_postexe(VALUE self) { + rb_ractor_ensure_main_ractor("can not use END{} in non-main Ractors"); rb_set_end_proc(rb_call_end_proc, rb_block_proc()); return Qnil; } From 49546f9075828b78320ff5af18db470a60af5587 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 7 Jul 2026 14:56:00 -0400 Subject: [PATCH 02/11] ZJIT: Expand anytostring into CondBranch Instead of special-casing it in the optimizer, expand the run-time type check into HIR and let the optimizer fold it if/when it can. Leave the `AnyToString` opcode as the fallback. --- zjit/src/hir.rs | 35 +++--- zjit/src/hir/opt_tests.rs | 41 ++++--- zjit/src/hir/tests.rs | 241 +++++++++++++++++++++++++++----------- 3 files changed, 216 insertions(+), 101 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 028196d7d4ff59..edc21a5bb1833d 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4331,13 +4331,6 @@ impl Function { self.push_insn_id(block, insn_id); continue; } } - Insn::AnyToString { str, .. } => { - if self.is_a(str, types::String) { - self.make_equal_to(insn_id, str); - } else { - self.push_insn_id(block, insn_id); - } - } Insn::IsMethodCfunc { val, cd, cfunc, state } if self.type_of(val).ruby_object_known() => { let class = self.type_of(val).ruby_object().unwrap(); let cme = unsafe { rb_zjit_vm_search_method(self.iseq.into(), cd as *mut rb_call_data, class) }; @@ -5623,11 +5616,6 @@ impl Function { insn_id } } - Insn::AnyToString { str, .. } if self.is_a(str, types::String) => { - self.make_equal_to(insn_id, str); - // Don't bother re-inferring the type of str; we already know it. - continue; - } Insn::IsA { val, class } => 'is_a: { let class_type = self.type_of(class); if !class_type.is_subtype(types::Class) { @@ -9152,8 +9140,27 @@ fn add_iseq_to_hir( let str = state.stack_pop()?; let val = state.stack_pop()?; - let anytostring = fun.push_insn(block, Insn::AnyToString { val, str, state: exit_id }); - state.stack_push(anytostring); + // Mirror logic of rb_obj_as_string_result() (`anytostring` in insns.def) + let has_type = fun.push_insn(block, Insn::HasType { val: str, expected: types::String }); + let iftrue_block = fun.new_block(insn_idx); + let iffalse_block = fun.new_block(insn_idx); + let join_block = fun.new_block(insn_idx); + fun.push_insn(block, Insn::CondBranch { + val: has_type, + if_true: BranchEdge { target: iftrue_block, args: vec![] }, + if_false: BranchEdge { target: iffalse_block, args: vec![] } + }); + // true block + let refined = fun.push_insn(iftrue_block, Insn::RefineType { val: str, new_type: types::String }); + fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![refined] })); + // false block + let refined = fun.push_insn(iffalse_block, Insn::RefineType { val: str, new_type: types::NotString }); + let anytostring = fun.push_insn(iffalse_block, Insn::AnyToString { val, str: refined, state: exit_id }); + fun.push_insn(iffalse_block, Insn::Jump(BranchEdge { target: join_block, args: vec![anytostring] })); + // join block + block = join_block; + let result = fun.push_insn(join_block, Insn::Param); + state.stack_push(result); } YARVINSN_getspecial => { let key = get_arg(pc, 0).as_u64(); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 595efc4e66b2a8..93624143a80291 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6966,9 +6966,9 @@ mod hir_opt_tests { v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v13:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v14:StringExact = StringCopy v13 - v28:StringExact = StringConcat v10, v14 + v35:StringExact = StringConcat v10, v14 CheckInterrupts - Return v28 + Return v35 "); } @@ -6991,10 +6991,10 @@ mod hir_opt_tests { v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018) - v34:StringExact = CCallVariadic v12, :Integer#to_s@0x1040 - v26:StringExact = StringConcat v10, v34 + v41:StringExact = CCallVariadic v12, :Integer#to_s@0x1040 + v33:StringExact = StringConcat v10, v41 CheckInterrupts - Return v26 + Return v33 "); } @@ -7024,9 +7024,9 @@ mod hir_opt_tests { v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(String@0x1010) v19:String = GuardType v10, String - v23:StringExact = StringConcat v14, v19 + v30:StringExact = StringConcat v14, v19 CheckInterrupts - Return v23 + Return v30 "); } @@ -7059,9 +7059,9 @@ mod hir_opt_tests { v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(MyString@0x1010) v19:String = GuardType v10, String - v23:StringExact = StringConcat v14, v19 + v30:StringExact = StringConcat v14, v19 CheckInterrupts - Return v23 + Return v30 "); } @@ -7092,11 +7092,20 @@ mod hir_opt_tests { v18:ArrayExact = GuardType v10, ArrayExact PatchPoint NoSingletonClass(Array@0x1010) PatchPoint MethodRedefined(Array@0x1010, to_s@0x1018, cme:0x1020) - v33:BasicObject = CCallWithFrame v18, :Array#to_s@0x1048 - v21:String = AnyToString v18, str: v33 - v23:StringExact = StringConcat v14, v21 + v40:BasicObject = CCallWithFrame v18, :Array#to_s@0x1048 + v21:CBool = HasType v40, String + CondBranch v21, bb4(), bb5() + bb4(): + v23:String = RefineType v40, String + Jump bb6(v23) + bb5(): + v25:NotString = RefineType v40, NotString + v26:String = AnyToString v10, str: v25 + Jump bb6(v26) + bb6(v28:String): + v30:StringExact = StringConcat v14, v28 CheckInterrupts - Return v23 + Return v30 "); } @@ -10366,10 +10375,10 @@ mod hir_opt_tests { v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v18:Fixnum = GuardType v10, Fixnum PatchPoint MethodRedefined(Integer@0x1010, to_s@0x1018, cme:0x1020) - v32:StringExact = CCallVariadic v18, :Integer#to_s@0x1048 - v23:StringExact = StringConcat v14, v32 + v39:StringExact = CCallVariadic v18, :Integer#to_s@0x1048 + v30:StringExact = StringConcat v14, v39 CheckInterrupts - Return v23 + Return v30 "); } diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 02de128ef4ee32..cbbea0cc86c42d 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2319,11 +2319,20 @@ pub(crate) mod hir_build_tests { v20:BasicObject = Send v19, :to_s # SendFallbackReason: ObjToString: result is not a string Jump bb6(v20) bb6(v22:BasicObject): - v24:String = AnyToString v12, str: v22 - v26:StringExact = StringConcat v10, v24 - v28:Symbol = StringIntern v26 + v24:CBool = HasType v22, String + CondBranch v24, bb7(), bb8() + bb7(): + v26:String = RefineType v22, String + Jump bb9(v26) + bb8(): + v28:NotString = RefineType v22, NotString + v29:String = AnyToString v12, str: v28 + Jump bb9(v29) + bb9(v31:String): + v33:StringExact = StringConcat v10, v31 + v35:Symbol = StringIntern v33 CheckInterrupts - Return v28 + Return v35 "); } @@ -5348,10 +5357,19 @@ pub(crate) mod hir_build_tests { v20:BasicObject = Send v19, :to_s # SendFallbackReason: ObjToString: result is not a string Jump bb6(v20) bb6(v22:BasicObject): - v24:String = AnyToString v12, str: v22 - v26:StringExact = StringConcat v10, v24 + v24:CBool = HasType v22, String + CondBranch v24, bb7(), bb8() + bb7(): + v26:String = RefineType v22, String + Jump bb9(v26) + bb8(): + v28:NotString = RefineType v22, NotString + v29:String = AnyToString v12, str: v28 + Jump bb9(v29) + bb9(v31:String): + v33:StringExact = StringConcat v10, v31 CheckInterrupts - Return v26 + Return v33 "); } @@ -5383,34 +5401,61 @@ pub(crate) mod hir_build_tests { v18:BasicObject = Send v17, :to_s # SendFallbackReason: ObjToString: result is not a string Jump bb6(v18) bb6(v20:BasicObject): - v22:String = AnyToString v10, str: v20 - v24:Fixnum[2] = Const Value(2) - v27:CBool[false] = HasType v24, String - CondBranch v27, bb7(), bb8() + v22:CBool = HasType v20, String + CondBranch v22, bb7(), bb8() bb7(): - v29 = RefineType v24, String - Jump bb9(v29) + v24:String = RefineType v20, String + Jump bb9(v24) bb8(): - v31:Fixnum[2] = RefineType v24, NotString - v32:BasicObject = Send v31, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb9(v32) - bb9(v34:BasicObject): - v36:String = AnyToString v24, str: v34 - v38:Fixnum[3] = Const Value(3) - v41:CBool[false] = HasType v38, String - CondBranch v41, bb10(), bb11() + v26:NotString = RefineType v20, NotString + v27:String = AnyToString v10, str: v26 + Jump bb9(v27) + bb9(v29:String): + v31:Fixnum[2] = Const Value(2) + v34:CBool[false] = HasType v31, String + CondBranch v34, bb10(), bb11() bb10(): - v43 = RefineType v38, String - Jump bb12(v43) + v36 = RefineType v31, String + Jump bb12(v36) bb11(): - v45:Fixnum[3] = RefineType v38, NotString - v46:BasicObject = Send v45, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb12(v46) - bb12(v48:BasicObject): - v50:String = AnyToString v38, str: v48 - v52:StringExact = StringConcat v22, v36, v50 - CheckInterrupts - Return v52 + v38:Fixnum[2] = RefineType v31, NotString + v39:BasicObject = Send v38, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb12(v39) + bb12(v41:BasicObject): + v43:CBool = HasType v41, String + CondBranch v43, bb13(), bb14() + bb13(): + v45:String = RefineType v41, String + Jump bb15(v45) + bb14(): + v47:NotString = RefineType v41, NotString + v48:String = AnyToString v31, str: v47 + Jump bb15(v48) + bb15(v50:String): + v52:Fixnum[3] = Const Value(3) + v55:CBool[false] = HasType v52, String + CondBranch v55, bb16(), bb17() + bb16(): + v57 = RefineType v52, String + Jump bb18(v57) + bb17(): + v59:Fixnum[3] = RefineType v52, NotString + v60:BasicObject = Send v59, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb18(v60) + bb18(v62:BasicObject): + v64:CBool = HasType v62, String + CondBranch v64, bb19(), bb20() + bb19(): + v66:String = RefineType v62, String + Jump bb21(v66) + bb20(): + v68:NotString = RefineType v62, NotString + v69:String = AnyToString v52, str: v68 + Jump bb21(v69) + bb21(v71:String): + v73:StringExact = StringConcat v29, v50, v71 + CheckInterrupts + Return v73 "); } @@ -5443,10 +5488,19 @@ pub(crate) mod hir_build_tests { v20:BasicObject = Send v19, :to_s # SendFallbackReason: ObjToString: result is not a string Jump bb6(v20) bb6(v22:BasicObject): - v24:String = AnyToString v12, str: v22 - v26:StringExact = StringConcat v10, v24 + v24:CBool = HasType v22, String + CondBranch v24, bb7(), bb8() + bb7(): + v26:String = RefineType v22, String + Jump bb9(v26) + bb8(): + v28:NotString = RefineType v22, NotString + v29:String = AnyToString v12, str: v28 + Jump bb9(v29) + bb9(v31:String): + v33:StringExact = StringConcat v10, v31 CheckInterrupts - Return v26 + Return v33 "); } @@ -5478,34 +5532,61 @@ pub(crate) mod hir_build_tests { v18:BasicObject = Send v17, :to_s # SendFallbackReason: ObjToString: result is not a string Jump bb6(v18) bb6(v20:BasicObject): - v22:String = AnyToString v10, str: v20 - v24:Fixnum[2] = Const Value(2) - v27:CBool[false] = HasType v24, String - CondBranch v27, bb7(), bb8() + v22:CBool = HasType v20, String + CondBranch v22, bb7(), bb8() bb7(): - v29 = RefineType v24, String - Jump bb9(v29) + v24:String = RefineType v20, String + Jump bb9(v24) bb8(): - v31:Fixnum[2] = RefineType v24, NotString - v32:BasicObject = Send v31, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb9(v32) - bb9(v34:BasicObject): - v36:String = AnyToString v24, str: v34 - v38:Fixnum[3] = Const Value(3) - v41:CBool[false] = HasType v38, String - CondBranch v41, bb10(), bb11() + v26:NotString = RefineType v20, NotString + v27:String = AnyToString v10, str: v26 + Jump bb9(v27) + bb9(v29:String): + v31:Fixnum[2] = Const Value(2) + v34:CBool[false] = HasType v31, String + CondBranch v34, bb10(), bb11() bb10(): - v43 = RefineType v38, String - Jump bb12(v43) + v36 = RefineType v31, String + Jump bb12(v36) bb11(): - v45:Fixnum[3] = RefineType v38, NotString - v46:BasicObject = Send v45, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb12(v46) - bb12(v48:BasicObject): - v50:String = AnyToString v38, str: v48 - v52:RegexpExact = ToRegexp v22, v36, v50 - CheckInterrupts - Return v52 + v38:Fixnum[2] = RefineType v31, NotString + v39:BasicObject = Send v38, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb12(v39) + bb12(v41:BasicObject): + v43:CBool = HasType v41, String + CondBranch v43, bb13(), bb14() + bb13(): + v45:String = RefineType v41, String + Jump bb15(v45) + bb14(): + v47:NotString = RefineType v41, NotString + v48:String = AnyToString v31, str: v47 + Jump bb15(v48) + bb15(v50:String): + v52:Fixnum[3] = Const Value(3) + v55:CBool[false] = HasType v52, String + CondBranch v55, bb16(), bb17() + bb16(): + v57 = RefineType v52, String + Jump bb18(v57) + bb17(): + v59:Fixnum[3] = RefineType v52, NotString + v60:BasicObject = Send v59, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb18(v60) + bb18(v62:BasicObject): + v64:CBool = HasType v62, String + CondBranch v64, bb19(), bb20() + bb19(): + v66:String = RefineType v62, String + Jump bb21(v66) + bb20(): + v68:NotString = RefineType v62, NotString + v69:String = AnyToString v52, str: v68 + Jump bb21(v69) + bb21(v71:String): + v73:RegexpExact = ToRegexp v29, v50, v71 + CheckInterrupts + Return v73 "); } @@ -5537,22 +5618,40 @@ pub(crate) mod hir_build_tests { v18:BasicObject = Send v17, :to_s # SendFallbackReason: ObjToString: result is not a string Jump bb6(v18) bb6(v20:BasicObject): - v22:String = AnyToString v10, str: v20 - v24:Fixnum[2] = Const Value(2) - v27:CBool[false] = HasType v24, String - CondBranch v27, bb7(), bb8() + v22:CBool = HasType v20, String + CondBranch v22, bb7(), bb8() bb7(): - v29 = RefineType v24, String - Jump bb9(v29) + v24:String = RefineType v20, String + Jump bb9(v24) bb8(): - v31:Fixnum[2] = RefineType v24, NotString - v32:BasicObject = Send v31, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb9(v32) - bb9(v34:BasicObject): - v36:String = AnyToString v24, str: v34 - v38:RegexpExact = ToRegexp v22, v36, MULTILINE|IGNORECASE|EXTENDED|NOENCODING + v26:NotString = RefineType v20, NotString + v27:String = AnyToString v10, str: v26 + Jump bb9(v27) + bb9(v29:String): + v31:Fixnum[2] = Const Value(2) + v34:CBool[false] = HasType v31, String + CondBranch v34, bb10(), bb11() + bb10(): + v36 = RefineType v31, String + Jump bb12(v36) + bb11(): + v38:Fixnum[2] = RefineType v31, NotString + v39:BasicObject = Send v38, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb12(v39) + bb12(v41:BasicObject): + v43:CBool = HasType v41, String + CondBranch v43, bb13(), bb14() + bb13(): + v45:String = RefineType v41, String + Jump bb15(v45) + bb14(): + v47:NotString = RefineType v41, NotString + v48:String = AnyToString v31, str: v47 + Jump bb15(v48) + bb15(v50:String): + v52:RegexpExact = ToRegexp v29, v50, MULTILINE|IGNORECASE|EXTENDED|NOENCODING CheckInterrupts - Return v38 + Return v52 "); } From c84ed8d90bde61e9b9789406746c19482e1a7a47 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 7 Jul 2026 15:50:15 -0400 Subject: [PATCH 03/11] ZJIT: Remove unused str operand from AnyToString We can call `rb_any_to_s` directly now because the type check is in HIR. --- zjit/bindgen/src/main.rs | 2 +- zjit/src/codegen.rs | 6 +- zjit/src/cruby_bindings.inc.rs | 2 +- zjit/src/hir.rs | 14 +- zjit/src/hir/opt_tests.rs | 41 +++--- zjit/src/hir/tests.rs | 233 ++++++++++++++++----------------- 6 files changed, 143 insertions(+), 155 deletions(-) diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 95f7ee87cc5e9c..cc28a6e05e8ceb 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -222,7 +222,7 @@ fn main() { .allowlist_type("ruby_rstring_private_flags") .allowlist_function("rb_ec_str_resurrect") .allowlist_function("rb_str_concat_literals") - .allowlist_function("rb_obj_as_string_result") + .allowlist_function("rb_any_to_s") .allowlist_function("rb_str_byte_substr") .allowlist_function("rb_str_substr_two_fixnums") .allowlist_function("rb_backref_get") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 0ad066c5e2bc24..a1eba061496d05 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -745,7 +745,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::FixnumBitCheck { val, index } => gen_fixnum_bit_check(asm, opnd!(val), *index), Insn::SideExit { state, reason, recompile } => no_output!(gen_side_exit(jit, asm, function, reason, *recompile, &function.frame_state(*state))), Insn::PutSpecialObject { value_type, state } => gen_putspecialobject(jit, asm, function, *value_type, &function.frame_state(*state)), - Insn::AnyToString { val, str, state } => gen_anytostring(asm, opnd!(val), opnd!(str), &function.frame_state(*state)), + Insn::AnyToString { val, state } => gen_anytostring(asm, opnd!(val), &function.frame_state(*state)), Insn::Defined { op_type, obj, pushval, v, lep_level, state } => gen_defined(jit, asm, function, *op_type, *obj, *pushval, opnd!(v), *lep_level, &function.frame_state(*state)), Insn::CheckMatch { target, pattern, flag, state } => gen_checkmatch(jit, asm, function, opnd!(target), opnd!(pattern), *flag, &function.frame_state(*state)), Insn::GetSpecialSymbol { symbol_type, state } => gen_getspecial_symbol(asm, *symbol_type, &function.frame_state(*state)), @@ -2604,10 +2604,10 @@ fn gen_box_fixnum(jit: &mut JITState, asm: &mut Assembler, function: &Function, asm.or(shifted, Opnd::UImm(RUBY_FIXNUM_FLAG as u64)) } -fn gen_anytostring(asm: &mut Assembler, val: lir::Opnd, str: lir::Opnd, state: &FrameState) -> lir::Opnd { +fn gen_anytostring(asm: &mut Assembler, val: lir::Opnd, state: &FrameState) -> lir::Opnd { gen_prepare_leaf_call_with_gc(asm, state); - asm_ccall!(asm, rb_obj_as_string_result, str, val) + asm_ccall!(asm, rb_any_to_s, val) } /// Evaluate if a value is truthy diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 3a76b2a16f10e7..7b000daa7393cb 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2104,6 +2104,7 @@ unsafe extern "C" { argv: *const VALUE, klass: VALUE, ) -> VALUE; + pub fn rb_any_to_s(obj: VALUE) -> VALUE; pub fn rb_obj_is_kind_of(obj: VALUE, klass: VALUE) -> VALUE; pub fn rb_obj_alloc(klass: VALUE) -> VALUE; pub fn rb_obj_frozen_p(obj: VALUE) -> VALUE; @@ -2199,7 +2200,6 @@ unsafe extern "C" { len: VALUE, empty: ::std::os::raw::c_int, ) -> VALUE; - pub fn rb_obj_as_string_result(str_: VALUE, obj: VALUE) -> VALUE; pub fn rb_str_concat_literals(num: usize, strary: *const VALUE) -> VALUE; pub fn rb_ec_str_resurrect( ec: *mut rb_execution_context_struct, diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index edc21a5bb1833d..36f0742a7ff83f 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1189,7 +1189,7 @@ pub enum Insn { /// Float#to_i: truncate float to integer via rb_jit_flo_to_i FloatToInt { recv: InsnId, state: InsnId }, - AnyToString { val: InsnId, str: InsnId, state: InsnId }, + AnyToString { val: InsnId, state: InsnId }, /// Refine the known type information of with additional type information. /// Computes the intersection of the existing type and the new type. @@ -1526,9 +1526,8 @@ macro_rules! for_each_operand_impl { $visit_one!(*val); $visit_one!(*state); } - Insn::AnyToString { val, str, state, .. } => { + Insn::AnyToString { val, state, .. } => { $visit_one!(*val); - $visit_one!(*str); $visit_one!(*state); } Insn::LoadField { recv, .. } => { @@ -2294,7 +2293,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::ArrayExtend { left, right, .. } => write!(f, "ArrayExtend {left}, {right}"), Insn::ArrayPush { array, val, .. } => write!(f, "ArrayPush {array}, {val}"), Insn::StringIntern { val, .. } => { write!(f, "StringIntern {val}") }, - Insn::AnyToString { val, str, .. } => { write!(f, "AnyToString {val}, str: {str}") }, + Insn::AnyToString { val, .. } => { write!(f, "AnyToString {val}") }, Insn::SideExit { reason, recompile, .. } => { if recompile.is_some() { write!(f, "SideExit {reason} recompile") @@ -6477,7 +6476,6 @@ impl Function { // Instructions with 2 Ruby object operands Insn::SetIvar { self_val: left, val: right, .. } | Insn::NewRange { low: left, high: right, .. } - | Insn::AnyToString { val: left, str: right, .. } | Insn::CheckMatch { target: left, pattern: right, .. } | Insn::WriteBarrier { recv: left, val: right } => { self.assert_subtype(insn_id, left, types::BasicObject)?; @@ -6487,6 +6485,9 @@ impl Function { self.assert_subtype(insn_id, klass, types::BasicObject)?; self.assert_subtype(insn_id, allow_nil, types::BoolExact) } + Insn::AnyToString { val, .. } => { + self.assert_subtype(insn_id, val, types::BasicObject) + } // Instructions with recv and a Vec of Ruby objects Insn::PushInlineFrame { recv, ref args, .. } | Insn::Send { recv, ref args, .. } @@ -9154,8 +9155,7 @@ fn add_iseq_to_hir( let refined = fun.push_insn(iftrue_block, Insn::RefineType { val: str, new_type: types::String }); fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![refined] })); // false block - let refined = fun.push_insn(iffalse_block, Insn::RefineType { val: str, new_type: types::NotString }); - let anytostring = fun.push_insn(iffalse_block, Insn::AnyToString { val, str: refined, state: exit_id }); + let anytostring = fun.push_insn(iffalse_block, Insn::AnyToString { val, state: exit_id }); fun.push_insn(iffalse_block, Insn::Jump(BranchEdge { target: join_block, args: vec![anytostring] })); // join block block = join_block; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 93624143a80291..559009b0b11ac0 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6966,9 +6966,9 @@ mod hir_opt_tests { v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v13:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v14:StringExact = StringCopy v13 - v35:StringExact = StringConcat v10, v14 + v34:StringExact = StringConcat v10, v14 CheckInterrupts - Return v35 + Return v34 "); } @@ -6991,10 +6991,10 @@ mod hir_opt_tests { v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018) - v41:StringExact = CCallVariadic v12, :Integer#to_s@0x1040 - v33:StringExact = StringConcat v10, v41 + v40:StringExact = CCallVariadic v12, :Integer#to_s@0x1040 + v32:StringExact = StringConcat v10, v40 CheckInterrupts - Return v33 + Return v32 "); } @@ -7024,9 +7024,9 @@ mod hir_opt_tests { v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(String@0x1010) v19:String = GuardType v10, String - v30:StringExact = StringConcat v14, v19 + v29:StringExact = StringConcat v14, v19 CheckInterrupts - Return v30 + Return v29 "); } @@ -7059,9 +7059,9 @@ mod hir_opt_tests { v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(MyString@0x1010) v19:String = GuardType v10, String - v30:StringExact = StringConcat v14, v19 + v29:StringExact = StringConcat v14, v19 CheckInterrupts - Return v30 + Return v29 "); } @@ -7092,20 +7092,19 @@ mod hir_opt_tests { v18:ArrayExact = GuardType v10, ArrayExact PatchPoint NoSingletonClass(Array@0x1010) PatchPoint MethodRedefined(Array@0x1010, to_s@0x1018, cme:0x1020) - v40:BasicObject = CCallWithFrame v18, :Array#to_s@0x1048 - v21:CBool = HasType v40, String + v39:BasicObject = CCallWithFrame v18, :Array#to_s@0x1048 + v21:CBool = HasType v39, String CondBranch v21, bb4(), bb5() bb4(): - v23:String = RefineType v40, String + v23:String = RefineType v39, String Jump bb6(v23) bb5(): - v25:NotString = RefineType v40, NotString - v26:String = AnyToString v10, str: v25 - Jump bb6(v26) - bb6(v28:String): - v30:StringExact = StringConcat v14, v28 + v25:String = AnyToString v10 + Jump bb6(v25) + bb6(v27:String): + v29:StringExact = StringConcat v14, v27 CheckInterrupts - Return v30 + Return v29 "); } @@ -10375,10 +10374,10 @@ mod hir_opt_tests { v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v18:Fixnum = GuardType v10, Fixnum PatchPoint MethodRedefined(Integer@0x1010, to_s@0x1018, cme:0x1020) - v39:StringExact = CCallVariadic v18, :Integer#to_s@0x1048 - v30:StringExact = StringConcat v14, v39 + v38:StringExact = CCallVariadic v18, :Integer#to_s@0x1048 + v29:StringExact = StringConcat v14, v38 CheckInterrupts - Return v30 + Return v29 "); } diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index cbbea0cc86c42d..a30f31dc00f64a 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2325,14 +2325,13 @@ pub(crate) mod hir_build_tests { v26:String = RefineType v22, String Jump bb9(v26) bb8(): - v28:NotString = RefineType v22, NotString - v29:String = AnyToString v12, str: v28 - Jump bb9(v29) - bb9(v31:String): - v33:StringExact = StringConcat v10, v31 - v35:Symbol = StringIntern v33 + v28:String = AnyToString v12 + Jump bb9(v28) + bb9(v30:String): + v32:StringExact = StringConcat v10, v30 + v34:Symbol = StringIntern v32 CheckInterrupts - Return v35 + Return v34 "); } @@ -5363,13 +5362,12 @@ pub(crate) mod hir_build_tests { v26:String = RefineType v22, String Jump bb9(v26) bb8(): - v28:NotString = RefineType v22, NotString - v29:String = AnyToString v12, str: v28 - Jump bb9(v29) - bb9(v31:String): - v33:StringExact = StringConcat v10, v31 + v28:String = AnyToString v12 + Jump bb9(v28) + bb9(v30:String): + v32:StringExact = StringConcat v10, v30 CheckInterrupts - Return v33 + Return v32 "); } @@ -5407,55 +5405,52 @@ pub(crate) mod hir_build_tests { v24:String = RefineType v20, String Jump bb9(v24) bb8(): - v26:NotString = RefineType v20, NotString - v27:String = AnyToString v10, str: v26 - Jump bb9(v27) - bb9(v29:String): - v31:Fixnum[2] = Const Value(2) - v34:CBool[false] = HasType v31, String - CondBranch v34, bb10(), bb11() + v26:String = AnyToString v10 + Jump bb9(v26) + bb9(v28:String): + v30:Fixnum[2] = Const Value(2) + v33:CBool[false] = HasType v30, String + CondBranch v33, bb10(), bb11() bb10(): - v36 = RefineType v31, String - Jump bb12(v36) + v35 = RefineType v30, String + Jump bb12(v35) bb11(): - v38:Fixnum[2] = RefineType v31, NotString - v39:BasicObject = Send v38, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb12(v39) - bb12(v41:BasicObject): - v43:CBool = HasType v41, String - CondBranch v43, bb13(), bb14() + v37:Fixnum[2] = RefineType v30, NotString + v38:BasicObject = Send v37, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb12(v38) + bb12(v40:BasicObject): + v42:CBool = HasType v40, String + CondBranch v42, bb13(), bb14() bb13(): - v45:String = RefineType v41, String - Jump bb15(v45) + v44:String = RefineType v40, String + Jump bb15(v44) bb14(): - v47:NotString = RefineType v41, NotString - v48:String = AnyToString v31, str: v47 - Jump bb15(v48) - bb15(v50:String): - v52:Fixnum[3] = Const Value(3) - v55:CBool[false] = HasType v52, String - CondBranch v55, bb16(), bb17() + v46:String = AnyToString v30 + Jump bb15(v46) + bb15(v48:String): + v50:Fixnum[3] = Const Value(3) + v53:CBool[false] = HasType v50, String + CondBranch v53, bb16(), bb17() bb16(): - v57 = RefineType v52, String - Jump bb18(v57) + v55 = RefineType v50, String + Jump bb18(v55) bb17(): - v59:Fixnum[3] = RefineType v52, NotString - v60:BasicObject = Send v59, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb18(v60) - bb18(v62:BasicObject): - v64:CBool = HasType v62, String - CondBranch v64, bb19(), bb20() + v57:Fixnum[3] = RefineType v50, NotString + v58:BasicObject = Send v57, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb18(v58) + bb18(v60:BasicObject): + v62:CBool = HasType v60, String + CondBranch v62, bb19(), bb20() bb19(): - v66:String = RefineType v62, String - Jump bb21(v66) + v64:String = RefineType v60, String + Jump bb21(v64) bb20(): - v68:NotString = RefineType v62, NotString - v69:String = AnyToString v52, str: v68 - Jump bb21(v69) - bb21(v71:String): - v73:StringExact = StringConcat v29, v50, v71 + v66:String = AnyToString v50 + Jump bb21(v66) + bb21(v68:String): + v70:StringExact = StringConcat v28, v48, v68 CheckInterrupts - Return v73 + Return v70 "); } @@ -5494,13 +5489,12 @@ pub(crate) mod hir_build_tests { v26:String = RefineType v22, String Jump bb9(v26) bb8(): - v28:NotString = RefineType v22, NotString - v29:String = AnyToString v12, str: v28 - Jump bb9(v29) - bb9(v31:String): - v33:StringExact = StringConcat v10, v31 + v28:String = AnyToString v12 + Jump bb9(v28) + bb9(v30:String): + v32:StringExact = StringConcat v10, v30 CheckInterrupts - Return v33 + Return v32 "); } @@ -5538,55 +5532,52 @@ pub(crate) mod hir_build_tests { v24:String = RefineType v20, String Jump bb9(v24) bb8(): - v26:NotString = RefineType v20, NotString - v27:String = AnyToString v10, str: v26 - Jump bb9(v27) - bb9(v29:String): - v31:Fixnum[2] = Const Value(2) - v34:CBool[false] = HasType v31, String - CondBranch v34, bb10(), bb11() + v26:String = AnyToString v10 + Jump bb9(v26) + bb9(v28:String): + v30:Fixnum[2] = Const Value(2) + v33:CBool[false] = HasType v30, String + CondBranch v33, bb10(), bb11() bb10(): - v36 = RefineType v31, String - Jump bb12(v36) + v35 = RefineType v30, String + Jump bb12(v35) bb11(): - v38:Fixnum[2] = RefineType v31, NotString - v39:BasicObject = Send v38, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb12(v39) - bb12(v41:BasicObject): - v43:CBool = HasType v41, String - CondBranch v43, bb13(), bb14() + v37:Fixnum[2] = RefineType v30, NotString + v38:BasicObject = Send v37, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb12(v38) + bb12(v40:BasicObject): + v42:CBool = HasType v40, String + CondBranch v42, bb13(), bb14() bb13(): - v45:String = RefineType v41, String - Jump bb15(v45) + v44:String = RefineType v40, String + Jump bb15(v44) bb14(): - v47:NotString = RefineType v41, NotString - v48:String = AnyToString v31, str: v47 - Jump bb15(v48) - bb15(v50:String): - v52:Fixnum[3] = Const Value(3) - v55:CBool[false] = HasType v52, String - CondBranch v55, bb16(), bb17() + v46:String = AnyToString v30 + Jump bb15(v46) + bb15(v48:String): + v50:Fixnum[3] = Const Value(3) + v53:CBool[false] = HasType v50, String + CondBranch v53, bb16(), bb17() bb16(): - v57 = RefineType v52, String - Jump bb18(v57) + v55 = RefineType v50, String + Jump bb18(v55) bb17(): - v59:Fixnum[3] = RefineType v52, NotString - v60:BasicObject = Send v59, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb18(v60) - bb18(v62:BasicObject): - v64:CBool = HasType v62, String - CondBranch v64, bb19(), bb20() + v57:Fixnum[3] = RefineType v50, NotString + v58:BasicObject = Send v57, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb18(v58) + bb18(v60:BasicObject): + v62:CBool = HasType v60, String + CondBranch v62, bb19(), bb20() bb19(): - v66:String = RefineType v62, String - Jump bb21(v66) + v64:String = RefineType v60, String + Jump bb21(v64) bb20(): - v68:NotString = RefineType v62, NotString - v69:String = AnyToString v52, str: v68 - Jump bb21(v69) - bb21(v71:String): - v73:RegexpExact = ToRegexp v29, v50, v71 + v66:String = AnyToString v50 + Jump bb21(v66) + bb21(v68:String): + v70:RegexpExact = ToRegexp v28, v48, v68 CheckInterrupts - Return v73 + Return v70 "); } @@ -5624,34 +5615,32 @@ pub(crate) mod hir_build_tests { v24:String = RefineType v20, String Jump bb9(v24) bb8(): - v26:NotString = RefineType v20, NotString - v27:String = AnyToString v10, str: v26 - Jump bb9(v27) - bb9(v29:String): - v31:Fixnum[2] = Const Value(2) - v34:CBool[false] = HasType v31, String - CondBranch v34, bb10(), bb11() + v26:String = AnyToString v10 + Jump bb9(v26) + bb9(v28:String): + v30:Fixnum[2] = Const Value(2) + v33:CBool[false] = HasType v30, String + CondBranch v33, bb10(), bb11() bb10(): - v36 = RefineType v31, String - Jump bb12(v36) + v35 = RefineType v30, String + Jump bb12(v35) bb11(): - v38:Fixnum[2] = RefineType v31, NotString - v39:BasicObject = Send v38, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb12(v39) - bb12(v41:BasicObject): - v43:CBool = HasType v41, String - CondBranch v43, bb13(), bb14() + v37:Fixnum[2] = RefineType v30, NotString + v38:BasicObject = Send v37, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb12(v38) + bb12(v40:BasicObject): + v42:CBool = HasType v40, String + CondBranch v42, bb13(), bb14() bb13(): - v45:String = RefineType v41, String - Jump bb15(v45) + v44:String = RefineType v40, String + Jump bb15(v44) bb14(): - v47:NotString = RefineType v41, NotString - v48:String = AnyToString v31, str: v47 - Jump bb15(v48) - bb15(v50:String): - v52:RegexpExact = ToRegexp v29, v50, MULTILINE|IGNORECASE|EXTENDED|NOENCODING + v46:String = AnyToString v30 + Jump bb15(v46) + bb15(v48:String): + v50:RegexpExact = ToRegexp v28, v48, MULTILINE|IGNORECASE|EXTENDED|NOENCODING CheckInterrupts - Return v52 + Return v50 "); } From 8590e2384468bed70e8e1589e82da347cdb6ae84 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 8 Jul 2026 12:43:48 -0400 Subject: [PATCH 04/11] ZJIT: Make AnyToString return StringExact It is just a call to `rb_any_to_s` which is a call to `rb_sprintf`. --- zjit/src/hir.rs | 2 +- zjit/src/hir/opt_tests.rs | 2 +- zjit/src/hir/tests.rs | 22 +++++++++++----------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 36f0742a7ff83f..d833199c6aa7e2 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -3157,7 +3157,7 @@ impl Function { Insn::GetClassVar { .. } => types::BasicObject, Insn::ToNewArray { .. } => types::ArrayExact, Insn::ToArray { .. } => types::ArrayExact, - Insn::AnyToString { .. } => types::String, + Insn::AnyToString { .. } => types::StringExact, Insn::IsBlockParamModified { .. } => types::CBool, Insn::GetBlockParam { .. } => types::BasicObject, // The type of Snapshot doesn't really matter; it's never materialized. It's used only diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 559009b0b11ac0..d5700c2c065b36 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -7099,7 +7099,7 @@ mod hir_opt_tests { v23:String = RefineType v39, String Jump bb6(v23) bb5(): - v25:String = AnyToString v10 + v25:StringExact = AnyToString v10 Jump bb6(v25) bb6(v27:String): v29:StringExact = StringConcat v14, v27 diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index a30f31dc00f64a..1ed38ad0c19305 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2325,7 +2325,7 @@ pub(crate) mod hir_build_tests { v26:String = RefineType v22, String Jump bb9(v26) bb8(): - v28:String = AnyToString v12 + v28:StringExact = AnyToString v12 Jump bb9(v28) bb9(v30:String): v32:StringExact = StringConcat v10, v30 @@ -5362,7 +5362,7 @@ pub(crate) mod hir_build_tests { v26:String = RefineType v22, String Jump bb9(v26) bb8(): - v28:String = AnyToString v12 + v28:StringExact = AnyToString v12 Jump bb9(v28) bb9(v30:String): v32:StringExact = StringConcat v10, v30 @@ -5405,7 +5405,7 @@ pub(crate) mod hir_build_tests { v24:String = RefineType v20, String Jump bb9(v24) bb8(): - v26:String = AnyToString v10 + v26:StringExact = AnyToString v10 Jump bb9(v26) bb9(v28:String): v30:Fixnum[2] = Const Value(2) @@ -5425,7 +5425,7 @@ pub(crate) mod hir_build_tests { v44:String = RefineType v40, String Jump bb15(v44) bb14(): - v46:String = AnyToString v30 + v46:StringExact = AnyToString v30 Jump bb15(v46) bb15(v48:String): v50:Fixnum[3] = Const Value(3) @@ -5445,7 +5445,7 @@ pub(crate) mod hir_build_tests { v64:String = RefineType v60, String Jump bb21(v64) bb20(): - v66:String = AnyToString v50 + v66:StringExact = AnyToString v50 Jump bb21(v66) bb21(v68:String): v70:StringExact = StringConcat v28, v48, v68 @@ -5489,7 +5489,7 @@ pub(crate) mod hir_build_tests { v26:String = RefineType v22, String Jump bb9(v26) bb8(): - v28:String = AnyToString v12 + v28:StringExact = AnyToString v12 Jump bb9(v28) bb9(v30:String): v32:StringExact = StringConcat v10, v30 @@ -5532,7 +5532,7 @@ pub(crate) mod hir_build_tests { v24:String = RefineType v20, String Jump bb9(v24) bb8(): - v26:String = AnyToString v10 + v26:StringExact = AnyToString v10 Jump bb9(v26) bb9(v28:String): v30:Fixnum[2] = Const Value(2) @@ -5552,7 +5552,7 @@ pub(crate) mod hir_build_tests { v44:String = RefineType v40, String Jump bb15(v44) bb14(): - v46:String = AnyToString v30 + v46:StringExact = AnyToString v30 Jump bb15(v46) bb15(v48:String): v50:Fixnum[3] = Const Value(3) @@ -5572,7 +5572,7 @@ pub(crate) mod hir_build_tests { v64:String = RefineType v60, String Jump bb21(v64) bb20(): - v66:String = AnyToString v50 + v66:StringExact = AnyToString v50 Jump bb21(v66) bb21(v68:String): v70:RegexpExact = ToRegexp v28, v48, v68 @@ -5615,7 +5615,7 @@ pub(crate) mod hir_build_tests { v24:String = RefineType v20, String Jump bb9(v24) bb8(): - v26:String = AnyToString v10 + v26:StringExact = AnyToString v10 Jump bb9(v26) bb9(v28:String): v30:Fixnum[2] = Const Value(2) @@ -5635,7 +5635,7 @@ pub(crate) mod hir_build_tests { v44:String = RefineType v40, String Jump bb15(v44) bb14(): - v46:String = AnyToString v30 + v46:StringExact = AnyToString v30 Jump bb15(v46) bb15(v48:String): v50:RegexpExact = ToRegexp v28, v48, MULTILINE|IGNORECASE|EXTENDED|NOENCODING From af2f0394fbf3f39ca846ef61bf6880caa9f1abe6 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 6 Jul 2026 18:20:18 -0400 Subject: [PATCH 05/11] ZJIT: Use Function::load_field in cruby_methods We converted other LoadField constructors to use this method. Use this method in cruby_methods too. --- zjit/src/cruby_methods.rs | 46 +++++++-------------------------------- zjit/src/hir.rs | 4 ++++ 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 3e4e9ed3768fed..29c32fe9aab5e9 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -322,20 +322,10 @@ fn inline_string_to_s(fun: &mut hir::Function, block: hir::BlockId, recv: hir::I fn inline_thread_current(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { let &[] = args else { return None; }; let ec = fun.push_insn(block, hir::Insn::LoadEC); - let thread_ptr = fun.push_insn(block, hir::Insn::LoadField { - recv: ec, - id: FieldName::thread_ptr, - offset: RUBY_OFFSET_EC_THREAD_PTR, - return_type: types::CPtr, - }); - let thread_self = fun.push_insn(block, hir::Insn::LoadField { - recv: thread_ptr, - id: FieldName::SelfParam, - offset: RUBY_OFFSET_THREAD_SELF, - // TODO(max): Add Thread type. But Thread.current is not guaranteed to be an exact Thread. - // You can make subclasses... - return_type: types::BasicObject, - }); + let thread_ptr = fun.load_field(block, ec, FieldName::thread_ptr, RUBY_OFFSET_EC_THREAD_PTR, types::CPtr); + // TODO(max): Add Thread type. But Thread.current is not guaranteed to be an exact Thread. + // You can make subclasses... + let thread_self = fun.load_field(block, thread_ptr, FieldName::SelfParam, RUBY_OFFSET_THREAD_SELF, types::BasicObject); Some(thread_self) } @@ -468,12 +458,7 @@ fn inline_hash_aset(fun: &mut hir::Function, block: hir::BlockId, recv: hir::Ins fn inline_string_bytesize(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { if args.is_empty() && fun.likely_a(recv, types::String, state) { let recv = fun.coerce_to(block, recv, types::String, state); - let len = fun.push_insn(block, hir::Insn::LoadField { - recv, - id: FieldName::len, - offset: RUBY_OFFSET_RSTRING_LEN, - return_type: types::CInt64, - }); + let len = fun.load_string_length(block, recv); let result = fun.push_insn(block, hir::Insn::BoxFixnum { val: len, @@ -492,12 +477,7 @@ fn inline_string_getbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir // when converting the index to a C integer. let index = fun.coerce_to(block, index, types::Fixnum, state); let unboxed_index = fun.push_insn(block, hir::Insn::UnboxFixnum { val: index }); - let len = fun.push_insn(block, hir::Insn::LoadField { - recv, - id: FieldName::len, - offset: RUBY_OFFSET_RSTRING_LEN, - return_type: types::CInt64, - }); + let len = fun.load_string_length(block, recv); // TODO(max): Find a way to mark these guards as not needed for correctness... as in, once // the data dependency is gone (say, the StringGetbyte is elided), they can also be elided. // @@ -520,12 +500,7 @@ fn inline_string_setbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir let value = fun.coerce_to(block, value, types::Fixnum, state); let unboxed_index = fun.push_insn(block, hir::Insn::UnboxFixnum { val: index }); - let len = fun.push_insn(block, hir::Insn::LoadField { - recv, - id: FieldName::len, - offset: RUBY_OFFSET_RSTRING_LEN, - return_type: types::CInt64, - }); + let len = fun.load_string_length(block, recv); let unboxed_index = fun.push_insn(block, hir::Insn::GuardLess { left: unboxed_index, right: len, reason: Box::new(SideExitReason::GuardLess), state }); let unboxed_index = fun.push_insn(block, hir::Insn::AdjustBounds { index: unboxed_index, length: len }); let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) }); @@ -543,12 +518,7 @@ fn inline_string_setbyte(fun: &mut hir::Function, block: hir::BlockId, recv: hir fn inline_string_empty_p(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { let &[] = args else { return None; }; - let len = fun.push_insn(block, hir::Insn::LoadField { - recv, - id: FieldName::len, - offset: RUBY_OFFSET_RSTRING_LEN, - return_type: types::CInt64, - }); + let len = fun.load_string_length(block, recv); let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) }); let is_zero = fun.push_insn(block, hir::Insn::IsBitEqual { left: len, right: zero }); let result = fun.push_insn(block, hir::Insn::BoxBool { val: is_zero }); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index d833199c6aa7e2..6165bd5e7620e8 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2792,6 +2792,10 @@ impl Function { self.push_insn(block, Insn::LoadField { recv, id, offset, return_type }) } + pub fn load_string_length(&mut self, block: BlockId, str: InsnId) -> InsnId { + self.load_field(block, str, FieldName::len, RUBY_OFFSET_RSTRING_LEN, types::CInt64) + } + // Add an instruction to an SSA block fn push_insn_id(&mut self, block: BlockId, insn_id: InsnId) -> InsnId { self.blocks[block.0].insns.push(insn_id); From 96d98f4f50f3afeeb2da00036ef7d57cfd17ebca Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 6 Jul 2026 18:22:01 -0400 Subject: [PATCH 06/11] ZJIT: Thread through num_bits to LoadField/StoreField Don't try to infer it based on the type at codegen; it may be Empty and that will give the wrong size (because it will match the first case). See https://github.com/ruby/ruby/pull/17504 --- zjit/src/codegen.rs | 12 ++++++------ zjit/src/hir.rs | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index a1eba061496d05..7f62d83ae09b21 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -768,8 +768,8 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::LoadSP => gen_load_sp(), &Insn::GetEP { level } => gen_get_ep(asm, level), Insn::LoadSelf => gen_load_self(asm), - &Insn::LoadField { recv, id, offset, return_type } => gen_load_field(asm, opnd!(recv), id, offset, return_type), - &Insn::StoreField { recv, id, offset, val } => no_output!(gen_store_field(asm, opnd!(recv), id, offset, opnd!(val), function.type_of(val))), + &Insn::LoadField { recv, id, offset, return_type: _, num_bits } => gen_load_field(asm, opnd!(recv), id, offset, num_bits), + &Insn::StoreField { recv, id, offset, val, num_bits } => no_output!(gen_store_field(asm, opnd!(recv), id, offset, opnd!(val), num_bits)), &Insn::WriteBarrier { recv, val } => no_output!(gen_write_barrier(jit, asm, opnd!(recv), opnd!(val), function.type_of(val))), &Insn::IsBlockGiven { lep } => gen_is_block_given(asm, opnd!(lep)), Insn::ArrayInclude { elements, target, state } => gen_array_include(jit, asm, function, opnds!(elements), opnd!(target), &function.frame_state(*state)), @@ -1370,18 +1370,18 @@ fn gen_load_self(asm: &mut Assembler) -> Opnd { asm.load(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SELF)) } -fn gen_load_field(asm: &mut Assembler, recv: Opnd, id: FieldName, offset: i32, return_type: Type) -> Opnd { +fn gen_load_field(asm: &mut Assembler, recv: Opnd, id: FieldName, offset: i32, num_bits: u8) -> Opnd { gen_incr_counter(asm, Counter::load_field_count); asm_comment!(asm, "Load field id={id} offset={offset}"); let recv = asm.load_mem(recv); - asm.load(Opnd::mem(return_type.num_bits(), recv, offset)) + asm.load(Opnd::mem(num_bits, recv, offset)) } -fn gen_store_field(asm: &mut Assembler, recv: Opnd, id: FieldName, offset: i32, val: Opnd, val_type: Type) { +fn gen_store_field(asm: &mut Assembler, recv: Opnd, id: FieldName, offset: i32, val: Opnd, num_bits: u8) { gen_incr_counter(asm, Counter::store_field_count); asm_comment!(asm, "Store field id={id} offset={offset}"); let recv = asm.load_mem(recv); - asm.store(Opnd::mem(val_type.num_bits(), recv, offset), val); + asm.store(Opnd::mem(num_bits, recv, offset), val); } fn gen_write_barrier(jit: &mut JITState, asm: &mut Assembler, recv: Opnd, val: Opnd, val_type: Type) { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 6165bd5e7620e8..bc6ea2ba997355 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1022,10 +1022,10 @@ pub enum Insn { LoadSP, /// Load cfp->self LoadSelf, - LoadField { recv: InsnId, id: FieldName, offset: i32, return_type: Type }, + LoadField { recv: InsnId, id: FieldName, offset: i32, return_type: Type, num_bits: u8 }, /// Write `val` at an offset of `recv`. /// When writing a Ruby object to a Ruby object, one must use GuardNotFrozen (or equivalent) before and WriteBarrier after. - StoreField { recv: InsnId, id: FieldName, offset: i32, val: InsnId }, + StoreField { recv: InsnId, id: FieldName, offset: i32, val: InsnId, num_bits: u8 }, WriteBarrier { recv: InsnId, val: InsnId }, /// Check whether VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM is set in the (already loaded) environment flags. @@ -2268,10 +2268,10 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::LoadSP => write!(f, "LoadSP"), &Insn::GetEP { level } => write!(f, "GetEP {level}"), Insn::LoadSelf => write!(f, "LoadSelf"), - &Insn::LoadField { recv, id, offset, return_type: _ } => { + &Insn::LoadField { recv, id, offset, return_type: _, num_bits: _ } => { write!(f, "LoadField {recv}, :{id}@{:#x}", self.ptr_map.map_offset(offset)) } - &Insn::StoreField { recv, id, offset, val } => write!(f, "StoreField {recv}, :{id}@{:#x}, {val}", self.ptr_map.map_offset(offset)), + &Insn::StoreField { recv, id, offset, val, num_bits: _ } => write!(f, "StoreField {recv}, :{id}@{:#x}, {val}", self.ptr_map.map_offset(offset)), &Insn::WriteBarrier { recv, val } => write!(f, "WriteBarrier {recv}, {val}"), Insn::SetIvar { self_val, id, val, .. } => write!(f, "SetIvar {self_val}, :{}, {val}", id.contents_lossy()), Insn::GetGlobal { id, .. } => write!(f, "GetGlobal :{}", id.contents_lossy()), @@ -2789,7 +2789,8 @@ impl Function { } pub fn load_field(&mut self, block: BlockId, recv: InsnId, id: FieldName, offset: i32, return_type: Type) -> InsnId { - self.push_insn(block, Insn::LoadField { recv, id, offset, return_type }) + let num_bits = return_type.num_bits(); + self.push_insn(block, Insn::LoadField { recv, id, offset, return_type, num_bits }) } pub fn load_string_length(&mut self, block: BlockId, str: InsnId) -> InsnId { @@ -4099,7 +4100,7 @@ impl Function { }; let replacement = if let (OptimizedMethodType::StructAset, &[val]) = (opt_type, args.as_slice()) { - self.push_insn(block, Insn::StoreField { recv: target, id: mid.into(), offset, val }); + self.push_insn(block, Insn::StoreField { recv: target, id: mid.into(), offset, val, num_bits: types::BasicObject.num_bits() }); self.push_insn(block, Insn::WriteBarrier { recv, val }); val } else { // StructAref @@ -5221,13 +5222,13 @@ impl Function { let offset = SIZEOF_VALUE_I32 * ivar_index as i32; (as_heap, offset) }; - self.push_insn(block, Insn::StoreField { recv: ivar_storage, id: id.into(), offset, val }); + self.push_insn(block, Insn::StoreField { recv: ivar_storage, id: id.into(), offset, val, num_bits: types::BasicObject.num_bits() }); self.push_insn(block, Insn::WriteBarrier { recv: self_val, val }); if next_shape_id != profiled_type.shape() { // Write the new shape ID let shape_id = self.push_insn(block, Insn::Const { val: Const::CShape(next_shape_id) }); let shape_id_offset = unsafe { rb_shape_id_offset() }; - self.push_insn(block, Insn::StoreField { recv: self_val, id: FieldName::shape_id, offset: shape_id_offset, val: shape_id }); + self.push_insn(block, Insn::StoreField { recv: self_val, id: FieldName::shape_id, offset: shape_id_offset, val: shape_id, num_bits: types::CShape.num_bits() }); } Ok(()) } @@ -8170,6 +8171,7 @@ fn add_iseq_to_hir( id: FieldName::VM_ENV_DATA_INDEX_FLAGS, offset: SIZEOF_VALUE_I32 * (VM_ENV_DATA_INDEX_FLAGS as i32), val: modified, + num_bits: types::CInt64.num_bits(), }); } YARVINSN_getblockparamproxy => { @@ -9421,6 +9423,7 @@ fn compile_jit_entry_state(fun: &mut Function, jit_entry_block: BlockId, jit_ent id: local_id.into(), offset: -(SIZEOF_VALUE_I32 * ep_offset), val: local, + num_bits: types::BasicObject.num_bits(), }); } } From e807ba452c4ac9198682414c66775cc1a30ea744 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 6 Jul 2026 18:24:05 -0400 Subject: [PATCH 07/11] ZJIT: Revert "ZJIT: Fold always failing GuardType (#17504)" We have fixed the problem with `num_bits`. --- zjit/src/hir.rs | 13 ------------- zjit/src/hir/opt_tests.rs | 35 ----------------------------------- zjit/src/hir_type/mod.rs | 3 --- 3 files changed, 51 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index bc6ea2ba997355..915d491bf60d5b 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5522,19 +5522,6 @@ impl Function { let mut new_insns = vec![]; for insn_id in old_insns { let replacement_id = match self.find(insn_id) { - // TODO (nirvdrum 2026-06-26): Folding the guard to a SideExit is a workaround, - // not a proper fix. It relies on constant folding to keep an Empty-typed value - // (see below) from reaching codegen; disabling this pass would let that value - // through and the program would fail to compile on x86-64. Compilation correctness - // should not depend on an optimization pass, so this should be replaced by a - // comprehensive fix. - Insn::GuardType { val, guard_type, state, recompile } if !self.type_of(val).could_be(guard_type) => { - // The value's type is disjoint from the guard type, so the guard can never - // pass. Every execution would side-exit here, so we replace the guard with an - // unconditional exit. The terminator handling below then drops the rest of - // the block, which is now unreachable. - self.new_insn(Insn::SideExit { state, reason: Box::new(SideExitReason::GuardType(guard_type)), recompile }) - } Insn::GuardType { val, guard_type, .. } if self.is_a(val, guard_type) => { self.make_equal_to(insn_id, val); // Don't bother re-inferring the type of val; we already know it. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index d5700c2c065b36..79e6c57401cc61 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -2440,41 +2440,6 @@ mod hir_opt_tests { assert!(!insns.contains(&dead_const)); } - // A GuardType whose value type is disjoint from the guard type can never pass, so every - // execution side-exits there. fold_constants should replace the guard with an unconditional - // SideExit and drop the now-unreachable instructions that follow. - #[test] - fn test_fold_guard_type_that_can_never_pass_into_side_exit() { - let mut function = Function::new(std::ptr::null()); - let entry = function.entry_block; - - let state = function.push_insn(entry, Insn::Snapshot { state: Box::new(FrameState::new(std::ptr::null())) }); - // A nil constant is a NilClass, which is disjoint from Fixnum, so the guard below can - // never pass and the optimizer infers its result as Empty. - let nil = function.push_insn(entry, Insn::Const { val: Const::Value(Qnil) }); - let guard = function.push_insn(entry, Insn::GuardType { val: nil, guard_type: types::Fixnum, state, recompile: None }); - function.push_insn(entry, Insn::StoreField { recv: nil, id: FieldName::len, offset: 0, val: guard }); - function.push_insn(entry, Insn::Return { val: guard }); - function.seal_entries(); - - function.infer_types(); - function.fold_constants(); - - let insns: Vec = function.blocks[entry.0].insns.iter().map(|&id| function.find(id)).collect(); - assert!( - insns.iter().any(|insn| matches!(insn, Insn::SideExit { .. })), - "expected the always-failing guard to be folded into a SideExit, got {insns:?}", - ); - assert!( - !insns.iter().any(|insn| matches!(insn, Insn::GuardType { .. })), - "the always-failing GuardType should have been removed, got {insns:?}", - ); - assert!( - !insns.iter().any(|insn| matches!(insn, Insn::StoreField { .. } | Insn::Return { .. })), - "instructions after the unconditional SideExit are unreachable and should have been dropped, got {insns:?}", - ); - } - #[test] fn test_eliminate_new_array() { eval(" diff --git a/zjit/src/hir_type/mod.rs b/zjit/src/hir_type/mod.rs index d005562d9ef4ab..9a44575042a44d 100644 --- a/zjit/src/hir_type/mod.rs +++ b/zjit/src/hir_type/mod.rs @@ -641,9 +641,6 @@ impl Type { } pub fn num_bytes(&self) -> u8 { - assert!(!self.bit_equal(types::Empty), - "a value of type Empty is unreachable and should have been eliminated before codegen"); - if self.is_subtype(types::CUInt8) || self.is_subtype(types::CInt8) { return 1; } if self.is_subtype(types::CUInt16) || self.is_subtype(types::CInt16) { return 2; } if self.is_subtype(types::CUInt32) || self.is_subtype(types::CInt32) { return 4; } From bcd9129c212ab8747f9e4dfb9f06c2782868ff27 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 9 Jul 2026 12:31:53 +0000 Subject: [PATCH 08/11] MN threads: reclaim a terminated coroutine via nt->dead_co, not the transfer return value The nt scheduling loop reclaimed a terminated coroutine thread's context using the return value of coroutine_transfer() (through thread_sched_switch0), assuming it was the resuming context (the dead coroutine). That holds for the amd64 asm backend, but the ucontext backend returns the transfer *target* instead, so the loop never recognized the terminal transfer: the reclaim always saw a live thread, and the loop always unlocked the sched lock -- which the dying thread had already released in coroutine_thread_terminated -- and double-unlocked it. Benign on glibc, but FreeBSD's pthread_mutex_unlock returns EPERM (crash), and it corrupts the scheduler (hang) elsewhere. Have the terminating coroutine record its own context in nt->dead_co (per native thread) right before its final transfer; the loop reads it after switch0 returns, independent of coroutine_transfer()'s backend-dependent return value. It is set and read on the same native thread with nothing running in between, so it cannot be overwritten before it is consumed. coroutine_transfer0() and thread_sched_switch0() no longer need to return the resuming context, so revert them to void (the ASan-only local remains). Reproduced on Linux with --with-coroutine=ucontext (100% hang before, clean after); amd64 and ucontext both pass bootstraptest thread/ractor/fiber. Co-Authored-By: Claude Opus 4.8 --- thread_pthread.c | 46 +++++++++++++++++++++++---------------------- thread_pthread.h | 5 +++++ thread_pthread_mn.c | 3 +++ 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/thread_pthread.c b/thread_pthread.c index 4912711506b277..1b664afbb1456a 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -346,9 +346,9 @@ struct rb_thread_context { bool dead; }; -static bool thread_sched_reclaim_from(struct coroutine_context *returning_from); +static bool thread_sched_reclaim(struct coroutine_context *dead_co); #endif -static struct coroutine_context *coroutine_transfer0(struct coroutine_context *transfer_from, +static void coroutine_transfer0(struct coroutine_context *transfer_from, struct coroutine_context *transfer_to, bool to_dead); #define thread_sched_dump(s) thread_sched_dump_(__FILE__, __LINE__, s) @@ -1264,7 +1264,7 @@ rb_thread_sched_init(struct rb_thread_sched *sched, bool atfork) #endif } -static struct coroutine_context * +static void coroutine_transfer0(struct coroutine_context *transfer_from, struct coroutine_context *transfer_to, bool to_dead) { #ifdef RUBY_ASAN_ENABLED @@ -1279,6 +1279,7 @@ coroutine_transfer0(struct coroutine_context *transfer_from, struct coroutine_co __tsan_switch_to_fiber(transfer_to->tsan_fiber, 0); #endif + RBIMPL_ATTR_MAYBE_UNUSED() struct coroutine_context *returning_from = coroutine_transfer(transfer_from, transfer_to); /* if to_dead was passed, the caller is promising that this coroutine is finished and it should @@ -1288,11 +1289,9 @@ coroutine_transfer0(struct coroutine_context *transfer_from, struct coroutine_co __sanitizer_finish_switch_fiber(transfer_from->fake_stack, (const void**)&returning_from->stack_base, &returning_from->stack_size); #endif - - return returning_from; } -static struct coroutine_context * +static void thread_sched_switch0(struct coroutine_context *current_cont, rb_thread_t *next_th, struct rb_native_thread *nt, bool to_dead) { VM_ASSERT(!nt->dedicated); @@ -1307,7 +1306,7 @@ thread_sched_switch0(struct coroutine_context *current_cont, rb_thread_t *next_t ruby_thread_set_native(next_th); native_thread_assign(nt, next_th); - return coroutine_transfer0(current_cont, next_th->sched.context, to_dead); + coroutine_transfer0(current_cont, next_th->sched.context, to_dead); } static void @@ -2436,12 +2435,16 @@ nt_start(void *ptr) if (next_th && next_th->nt == NULL) { RUBY_DEBUG_LOG("nt:%d next_th:%d", (int)nt->serial, (int)next_th->serial); #if USE_MN_THREADS - struct coroutine_context *from = thread_sched_switch0(nt->nt_context, next_th, nt, false); - if (thread_sched_reclaim_from(from)) { - // A terminal transfer: the dying thread released - // the sched lock before transferring (its Ractor - // may be gone by now); we resumed with NO lock - // held and must not touch the sched again. + thread_sched_switch0(nt->nt_context, next_th, nt, false); + + // If a coroutine terminated during the transfer, co_start + // recorded it in nt->dead_co (switch0's return value is + // backend-dependent, unusable; see thread_pthread.h). + struct coroutine_context *dead_co = nt->dead_co; + nt->dead_co = NULL; + if (thread_sched_reclaim(dead_co)) { + // it already released the sched lock before its + // transfer (its Ractor may be gone): leave sched be. locked = false; } #else @@ -2477,16 +2480,15 @@ static int native_thread_create_shared(rb_thread_t *th); static void nt_free_stack(void *mstack); -// Reclaim the coroutine context the nt scheduling loop just resumed from, -// if its thread terminated. A dying thread always returns to its native -// thread's own context (co_start's epilogue), so this is the only place -// terminal transfers arrive -- and our running here proves the transfer's -// register save into the block has completed. Returns true for a terminal -// transfer, whose dying thread RELEASED the sched lock before transferring. +// Reclaim the context a coroutine thread recorded in nt->dead_co before its +// final transfer (co_start's epilogue). Our running here proves that transfer's +// register save into the block completed. Returns true when a thread did +// terminate -- it RELEASED the sched lock before transferring; NULL/false means +// a live yield, where the loop still owns the lock. static bool -thread_sched_reclaim_from(struct coroutine_context *returning_from) +thread_sched_reclaim(struct coroutine_context *dead_co) { - struct rb_thread_context *tctx = (struct rb_thread_context *)returning_from; + struct rb_thread_context *tctx = (struct rb_thread_context *)dead_co; if (tctx != NULL && tctx->dead) { nt_free_stack(tctx->stack); @@ -2512,7 +2514,7 @@ rb_threadptr_sched_free(rb_thread_t *th) else if (th->sched.context != NULL) { // a coroutine thread that never reached its epilogue (never started); // a terminated one is reclaimed by whoever resumed from its final - // transfer (thread_sched_reclaim_from), and cleared this pointer. + // transfer (thread_sched_reclaim), and cleared this pointer. struct rb_thread_context *tctx = (struct rb_thread_context *)th->sched.context; nt_free_stack(tctx->stack); SIZED_FREE(tctx); diff --git a/thread_pthread.h b/thread_pthread.h index cbc2d5ee4abf0a..cf717aef2f75f5 100644 --- a/thread_pthread.h +++ b/thread_pthread.h @@ -112,6 +112,11 @@ struct rb_native_thread { struct coroutine_context *nt_context; int dedicated; + + // A terminating coroutine records its context here before its final + // transfer; this nt's loop reclaims it. (Not via coroutine_transfer()'s + // return value: its meaning differs between the amd64 asm and ucontext.) + struct coroutine_context *dead_co; }; #undef except diff --git a/thread_pthread_mn.c b/thread_pthread_mn.c index 2efde13702e420..422aae7b00034e 100644 --- a/thread_pthread_mn.c +++ b/thread_pthread_mn.c @@ -549,6 +549,9 @@ co_start(struct coroutine_context *from, struct coroutine_context *self) // this native thread's own context, where the nt loop reclaims tctx. struct rb_thread_context *tctx = (struct rb_thread_context *)self; tctx->dead = true; + // Hand this context to the nt's loop, which reclaims it right after the + // final transfer returns from switch0. + tctx->nt->dead_co = &tctx->co; coroutine_transfer0(&tctx->co, tctx->nt->nt_context, true); rb_bug("unreachable"); From 14fe74327e08aba9fa806ab4c7662638c047e4eb Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 8 Jul 2026 22:32:10 -0700 Subject: [PATCH 09/11] ZJIT: Always specialize monomorphic getivar Co-authored-by: Max Bernstein --- zjit/src/distribution.rs | 4 + zjit/src/hir.rs | 184 ++++++++++++++++++++---------- zjit/src/hir/opt_tests.rs | 227 +++++++++++++++----------------------- zjit/src/hir/tests.rs | 24 +--- zjit/src/stats.rs | 2 + 5 files changed, 220 insertions(+), 221 deletions(-) diff --git a/zjit/src/distribution.rs b/zjit/src/distribution.rs index aa4667b9399c09..1d8149dbd35fe2 100644 --- a/zjit/src/distribution.rs +++ b/zjit/src/distribution.rs @@ -81,6 +81,10 @@ pub struct DistributionSummary DistributionSummary { + pub fn empty() -> Self { + Self { kind: DistributionKind::Empty, buckets: [Default::default(); N] } + } + pub fn new(dist: &Distribution) -> Self { #[cfg(debug_assertions)] { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 915d491bf60d5b..89ee6c013fa00b 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -570,6 +570,7 @@ pub enum SideExitReason { DirectiveInduced, SendWhileTracing, NoProfileSend, + NoProfileGetIvar, InvokeBlockNotIfunc, } @@ -3495,6 +3496,19 @@ impl Function { } } + fn profile_summary(&self, profiles: &ProfileOracle, recv: InsnId, state: InsnId) -> Option { + let Some(entries) = profiles.get(state) else { + return None; + }; + let recv = self.chase_insn(recv); + for (entry_insn, entry_type_summary) in entries { + if self.chase_insn(*entry_insn) == recv { + return Some(entry_type_summary.clone()); + } + } + None + } + fn polymorphic_summary(&self, profiles: &ProfileOracle, recv: InsnId, state: InsnId) -> Option { let Some(entries) = profiles.get(state) else { return None; @@ -6765,6 +6779,78 @@ impl Function { self.validate_types()?; Ok(()) } + + /// Return Some(InsnId) if we generated any code to load an ivar and None if we only generated + /// an unconditional SideExit (in which case we should end the block). + fn dispatch_getivar( + &mut self, + profiled_types: &Vec, + mut block: BlockId, + insn_idx: u32, + self_param: InsnId, + id: ID, + ic: *const iseq_inline_iv_cache_entry, + exit_id: InsnId, + ) -> Option<(BlockId, InsnId)> { + // 0 profiled_types: Generate a recompile exit or a fallback. No need for new HIR blocks. + if profiled_types.is_empty() { + if self.policy.no_side_exits { + self.count(block, Counter::getivar_fallback_no_side_exits); + let result = self.push_insn(block, Insn::GetIvar { self_val: self_param, id, ic, state: exit_id }); + return Some((block, result)); + } else { + self.push_insn(block, Insn::SideExit { state: exit_id, reason: Box::new(SideExitReason::NoProfileGetIvar), recompile: Some(Recompile) }); + return None; + } + } + // 1 profiled_types: Generate a monomorphic getivar with a guard if allowed by policy. No need for new HIR blocks. + if profiled_types.len() == 1 && !self.policy.no_side_exits { + // 0 profiled_types: Generate a recompile exit or a fallback. + let actual = self.load_shape(block, self_param); + self.guard_shape(block, actual, profiled_types[0].shape(), exit_id, Some(Recompile)); + let result = self.load_ivar(block, self_param, profiled_types[0], id); + return Some((block, result)); + } + + // Otherwise, make HIR blocks to handle different shapes or a fallback, and let them jump to join_block. + let edge = |target: BlockId| BranchEdge { target, args: vec![] }; + let branch = |cond: InsnId, if_true: BlockId, if_false: BlockId| Insn::CondBranch { val: cond, if_true: edge(if_true), if_false: edge(if_false) }; + let actual = self.load_shape(block, self_param); + let last_shape_index = profiled_types.len() - 1; + let join_block = self.new_block(insn_idx); + let result = self.push_insn(join_block, Insn::Param); + for (i, &profiled_type) in profiled_types.iter().enumerate() { + let load_optimized_block = self.new_block(insn_idx); + if i == last_shape_index { + if self.policy.no_side_exits { + // If the policy doesn't allow exits, make a fallback block and jump to it if the shape doesn't match. + let expected = self.push_insn(block, Insn::Const { val: Const::CShape(profiled_type.shape()) }); + let val = self.push_insn(block, Insn::IsBitEqual { left: actual, right: expected }); + let load_fallback_block = self.new_block(insn_idx); + self.push_insn(block, branch(val, load_optimized_block, load_fallback_block)); + self.count(load_fallback_block, Counter::getivar_fallback_no_side_exits); + let result = self.push_insn(load_fallback_block, Insn::GetIvar { self_val: self_param, id, ic, state: exit_id }); + self.push_insn(load_fallback_block, Insn::Jump(BranchEdge { target: join_block, args: vec![result] })); + } else { + // If the policy allows exits, exit to the interpreter if the shape doesn't match. + self.guard_shape(block, actual, profiled_type.shape(), exit_id, Some(Recompile)); + // TODO(max): Don't make a new block in this case + self.push_insn(block, Insn::Jump(BranchEdge { target: load_optimized_block, args: vec![] })); + } + } else { + // If this is not the last profiled shape, let the guard jump to the next block. + let expected = self.push_insn(block, Insn::Const { val: Const::CShape(profiled_type.shape()) }); + let val = self.push_insn(block, Insn::IsBitEqual { left: actual, right: expected}); + let next_block = self.new_block(insn_idx); + self.push_insn(block, branch(val, load_optimized_block, next_block)); + block = next_block; + } + // Generate the optimized getivar for the profiled_type and jump to join_block + let result = self.load_ivar(load_optimized_block, self_param, profiled_type, id); + self.push_insn(load_optimized_block, Insn::Jump(BranchEdge { target: join_block, args: vec![result] })); + } + Some((join_block, result)) + } } impl<'a> std::fmt::Display for FunctionPrinter<'a> { @@ -8907,68 +8993,44 @@ fn add_iseq_to_hir( fun.push_insn(block, Insn::SideExit { state: exit_id, reason: Box::new(SideExitReason::UnhandledYARVInsn(opcode)), recompile: None }); break; // End the block } - if let Some(summary) = fun.polymorphic_summary(&profiles, self_param, exit_id) { - self_param = fun.push_insn(block, Insn::GuardType { val: self_param, guard_type: types::HeapBasicObject, state: exit_id, recompile: None }); - let join_block = fun.new_block(insn_idx); - let join_param = fun.push_insn(join_block, Insn::Param); - // Dedup by expected shape so objects with different classes but the same shape can share code - // TODO(max): De-duplicate further by checking ivar offsets to allow - // different shapes with the same ivar layout to share code - let mut seen_shape = Vec::with_capacity(summary.buckets().len()); - for &profiled_type in summary.buckets() { - // End of the buckets - if profiled_type.is_empty() { break; } - // Instance variable lookups on immediate values are always nil; don't bother - if profiled_type.flags().is_immediate() { continue; } - let profiled_shape = profiled_type.shape(); - assert!(profiled_shape.is_valid()); - // Too-complex shapes use hash tables for ivars; - // rb_shape_get_iv_index doesn't work for them. - // Let the fallthrough GetIvar handle these. - if profiled_shape.is_complex() { continue; } - if seen_shape.contains(&profiled_shape) { continue; } - seen_shape.push(profiled_shape); - let actual_shape = fun.load_shape(block, self_param); - // Load the expected shape to a variable - let expected_shape = fun.push_insn(block, Insn::Const { val: Const::CShape(profiled_shape) }); - let has_shape = fun.push_insn(block, Insn::IsBitEqual { left: actual_shape, right: expected_shape }); - let iftrue_block = fun.new_block(insn_idx); - let target = BranchEdge { target: iftrue_block, args: vec![] }; - let fall_through = fun.new_block(insn_idx); - - fun.push_insn(block, Insn::CondBranch { val: has_shape, - if_true: target, - if_false: BranchEdge { target: fall_through, args: vec![] } - }); - - block = fall_through; - let result = fun.load_ivar(iftrue_block, self_param, profiled_type, id); - fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![result] })); - } - // In the fallthrough case, do a generic interpreter getivar and then join. - let result = fun.push_insn(block, Insn::GetIvar { self_val: self_param, id, ic, state: exit_id }); - fun.push_insn(block, Insn::Jump(BranchEdge { target: join_block, args: vec![result] })); - state.stack_push(join_param); - // Continue compilation from the join block at the next instruction. - // Make a copy of the current state without the args (pop the receiver - // and push the result) because we just use the locals/stack sizes to - // make the right number of Params - block = join_block; - } else { - if let Some(profiled_type) = fun.monomorphic_summary(&profiles, self_param, exit_id) { - let result = fun.try_emit_optimized_getivar(block, self_param, id, profiled_type, exit_id).unwrap_or_else(|counter| { - fun.count(block, counter); - fun.push_insn(block, Insn::GetIvar { self_val: self_param, id, ic, state: exit_id }) - }); - state.stack_push(result); - } else { - let resolution = fun.resolve_receiver_type_from_profile(self_param, exit_id); - let counter = Function::getivar_fallback_reason(resolution, ic); - fun.count(block, counter); - let result = fun.push_insn(block, Insn::GetIvar { self_val: self_param, id, ic, state: exit_id }); - state.stack_push(result); + let summary = fun.profile_summary(&profiles, self_param, exit_id).unwrap_or_else(|| { + TypeDistributionSummary::empty() + }); + let self_param = fun.guard_heap(block, self_param, exit_id); + // Filter out profiled types we don't care to optimize + let profiled_types = summary.buckets().iter().filter(|profiled_type| { + // Don't read past the end of the profiled types + !profiled_type.is_empty() + // Instance variable lookups on immediate values are always nil; don't bother + && !profiled_type.flags().is_immediate() + // Too-complex shapes use hash tables for ivars; + // rb_shape_get_iv_index doesn't work for them. + // Let the fallthrough GetIvar handle these. + && !profiled_type.shape().is_complex() + }).collect::>(); + // We might have two objects of class A and B with the same shape; de-duplicate + // profiled types by shape. This is just an optimization to reduce code size. + let mut profiled_types_unique_shapes = Vec::with_capacity(profiled_types.len()); + for &profiled_type in profiled_types { + if profiled_types_unique_shapes.iter().any(|t: &ProfiledType| t.shape() == profiled_type.shape()) { + continue; } - } + profiled_types_unique_shapes.push(profiled_type); + } + let Some((new_block, result)) = fun.dispatch_getivar( + &profiled_types_unique_shapes, + block, + insn_idx, + self_param, + id, + ic, + exit_id, + ) else { + // Side-exiting unconditionally; end the block + break; + }; + block = new_block; + state.stack_push(result); } YARVINSN_setinstancevariable => { let id = ID(get_arg(pc, 0).as_u64()); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 79e6c57401cc61..6db2f1d74e0b99 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -5616,9 +5616,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - v11:BasicObject = GetIvar v6, :@foo - CheckInterrupts - Return v11 + v11:HeapBasicObject = GuardType v6, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } @@ -8298,27 +8297,20 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:HeapBasicObject): PatchPoint SingleRactorMode - v13:CShape = LoadField v6, :shape_id@0x1000 + v12:CShape = LoadField v6, :shape_id@0x1000 v14:CShape[0x1001] = Const CShape(0x1001) - v15:CBool = IsBitEqual v13, v14 + v15:CBool = IsBitEqual v12, v14 CondBranch v15, bb5(), bb6() bb5(): v17:BasicObject = LoadField v6, :@foo@0x1002 Jump bb4(v17) bb6(): - v19:CShape = LoadField v6, :shape_id@0x1000 - v20:CShape[0x1003] = Const CShape(0x1003) - v21:CBool = IsBitEqual v19, v20 - CondBranch v21, bb7(), bb8() - bb7(): - v23:BasicObject = LoadField v6, :@foo@0x1004 - Jump bb4(v23) - bb8(): - v25:BasicObject = GetIvar v6, :@foo - Jump bb4(v25) - bb4(v12:BasicObject): + v19:CShape[0x1003] = GuardBitEquals v12, CShape(0x1003) recompile + v21:BasicObject = LoadField v6, :@foo@0x1004 + Jump bb4(v21) + bb4(v13:BasicObject): CheckInterrupts - Return v12 + Return v13 "); } @@ -8351,9 +8343,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - v11:BasicObject = GetIvar v6, :@foo - CheckInterrupts - Return v11 + v11:HeapBasicObject = GuardType v6, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } @@ -8377,9 +8368,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - v11:BasicObject = GetIvar v6, :@foo - CheckInterrupts - Return v11 + v11:HeapBasicObject = GuardType v6, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } @@ -8403,9 +8393,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - v11:BasicObject = GetIvar v6, :@foo - CheckInterrupts - Return v11 + v11:HeapBasicObject = GuardType v6, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } @@ -8429,9 +8418,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - v11:BasicObject = GetIvar v6, :@foo - CheckInterrupts - Return v11 + v11:HeapBasicObject = GuardType v6, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } @@ -8455,9 +8443,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - v11:BasicObject = GetIvar v6, :@foo - CheckInterrupts - Return v11 + v11:HeapBasicObject = GuardType v6, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } @@ -8481,9 +8468,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - v11:BasicObject = GetIvar v6, :@foo - CheckInterrupts - Return v11 + v11:HeapBasicObject = GuardType v6, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } @@ -8573,9 +8559,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - v11:BasicObject = GetIvar v6, :@foo - CheckInterrupts - Return v11 + v11:HeapBasicObject = GuardType v6, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } @@ -8782,9 +8767,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - v11:BasicObject = GetIvar v6, :@foo - CheckInterrupts - Return v11 + v11:HeapBasicObject = GuardType v6, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } @@ -8853,9 +8837,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - v11:BasicObject = GetIvar v6, :@foo - CheckInterrupts - Return v11 + v11:HeapBasicObject = GuardType v6, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } @@ -8954,9 +8937,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - v11:BasicObject = GetIvar v6, :@var1000 - CheckInterrupts - Return v11 + v11:HeapBasicObject = GuardType v6, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } @@ -9055,32 +9037,25 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint SingleRactorMode v11:HeapBasicObject = GuardType v6, HeapBasicObject - v13:CShape = LoadField v11, :shape_id@0x1000 + v12:CShape = LoadField v11, :shape_id@0x1000 v14:CShape[0x1001] = Const CShape(0x1001) - v15:CBool = IsBitEqual v13, v14 + v15:CBool = IsBitEqual v12, v14 CondBranch v15, bb5(), bb6() bb5(): v17:BasicObject = LoadField v11, :@foo@0x1002 Jump bb4(v17) bb6(): - v19:CShape = LoadField v11, :shape_id@0x1000 - v20:CShape[0x1003] = Const CShape(0x1003) - v21:CBool = IsBitEqual v19, v20 - CondBranch v21, bb7(), bb8() - bb7(): - v23:CPtr = LoadField v11, :as_heap@0x1004 - v24:BasicObject = LoadField v23, :@foo@0x1005 - Jump bb4(v24) - bb8(): - v26:BasicObject = GetIvar v11, :@foo - Jump bb4(v26) - bb4(v12:BasicObject): - v29:Fixnum[1] = Const Value(1) + v19:CShape[0x1003] = GuardBitEquals v12, CShape(0x1003) recompile + v21:CPtr = LoadField v11, :as_heap@0x1004 + v22:BasicObject = LoadField v21, :@foo@0x1005 + Jump bb4(v22) + bb4(v13:BasicObject): + v25:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v40:Fixnum = GuardType v12, Fixnum recompile - v41:Fixnum = FixnumAdd v40, v29 + v36:Fixnum = GuardType v13, Fixnum recompile + v37:Fixnum = FixnumAdd v36, v25 CheckInterrupts - Return v41 + Return v37 "); } @@ -9129,32 +9104,25 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint SingleRactorMode v11:HeapBasicObject = GuardType v6, HeapBasicObject - v13:CShape = LoadField v11, :shape_id@0x1000 + v12:CShape = LoadField v11, :shape_id@0x1000 v14:CShape[0x1001] = Const CShape(0x1001) - v15:CBool = IsBitEqual v13, v14 + v15:CBool = IsBitEqual v12, v14 CondBranch v15, bb5(), bb6() bb5(): v17:CPtr = LoadField v11, :as_heap@0x1002 v18:BasicObject = LoadField v17, :@foo@0x1003 Jump bb4(v18) bb6(): - v20:CShape = LoadField v11, :shape_id@0x1000 - v21:CShape[0x1004] = Const CShape(0x1004) - v22:CBool = IsBitEqual v20, v21 - CondBranch v22, bb7(), bb8() - bb7(): - v24:BasicObject = LoadField v11, :@foo@0x1005 - Jump bb4(v24) - bb8(): - v26:BasicObject = GetIvar v11, :@foo - Jump bb4(v26) - bb4(v12:BasicObject): - v29:Fixnum[1] = Const Value(1) + v20:CShape[0x1004] = GuardBitEquals v12, CShape(0x1004) recompile + v22:BasicObject = LoadField v11, :@foo@0x1005 + Jump bb4(v22) + bb4(v13:BasicObject): + v25:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v40:Fixnum = GuardType v12, Fixnum recompile - v41:Fixnum = FixnumAdd v40, v29 + v36:Fixnum = GuardType v13, Fixnum recompile + v37:Fixnum = FixnumAdd v36, v25 CheckInterrupts - Return v41 + Return v37 "); } @@ -9195,31 +9163,24 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint SingleRactorMode v11:HeapBasicObject = GuardType v6, HeapBasicObject - v13:CShape = LoadField v11, :shape_id@0x1000 + v12:CShape = LoadField v11, :shape_id@0x1000 v14:CShape[0x1001] = Const CShape(0x1001) - v15:CBool = IsBitEqual v13, v14 + v15:CBool = IsBitEqual v12, v14 CondBranch v15, bb5(), bb6() bb5(): v17:BasicObject = LoadField v11, :@foo@0x1002 Jump bb4(v17) bb6(): - v19:CShape = LoadField v11, :shape_id@0x1000 - v20:CShape[0x1003] = Const CShape(0x1003) - v21:CBool = IsBitEqual v19, v20 - CondBranch v21, bb7(), bb8() - bb7(): - v23:BasicObject = LoadField v11, :@foo@0x1002 - Jump bb4(v23) - bb8(): - v25:BasicObject = GetIvar v11, :@foo - Jump bb4(v25) - bb4(v12:BasicObject): - v28:Fixnum[1] = Const Value(1) + v19:CShape[0x1003] = GuardBitEquals v12, CShape(0x1003) recompile + v21:BasicObject = LoadField v11, :@foo@0x1002 + Jump bb4(v21) + bb4(v13:BasicObject): + v24:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v39:Fixnum = GuardType v12, Fixnum recompile - v40:Fixnum = FixnumAdd v39, v28 + v35:Fixnum = GuardType v13, Fixnum recompile + v36:Fixnum = FixnumAdd v35, v24 CheckInterrupts - Return v40 + Return v36 "); } @@ -9258,29 +9219,22 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint SingleRactorMode v11:HeapBasicObject = GuardType v6, HeapBasicObject - v13:CShape = LoadField v11, :shape_id@0x1000 + v12:CShape = LoadField v11, :shape_id@0x1000 v14:CShape[0x1001] = Const CShape(0x1001) - v15:CBool = IsBitEqual v13, v14 + v15:CBool = IsBitEqual v12, v14 CondBranch v15, bb5(), bb6() bb5(): v17:RubyValue = LoadField v11, :fields_obj@0x1002 v18:BasicObject = LoadField v17, :@a@0x1002 Jump bb4(v18) bb6(): - v20:CShape = LoadField v11, :shape_id@0x1000 - v21:CShape[0x1003] = Const CShape(0x1003) - v22:CBool = IsBitEqual v20, v21 - CondBranch v22, bb7(), bb8() - bb7(): - v24:RubyValue = LoadField v11, :fields_obj@0x1004 - v25:BasicObject = LoadField v24, :@a@0x1002 - Jump bb4(v25) - bb8(): - v27:BasicObject = GetIvar v11, :@a - Jump bb4(v27) - bb4(v12:BasicObject): + v20:CShape[0x1003] = GuardBitEquals v12, CShape(0x1003) recompile + v22:RubyValue = LoadField v11, :fields_obj@0x1004 + v23:BasicObject = LoadField v22, :@a@0x1002 + Jump bb4(v23) + bb4(v13:BasicObject): CheckInterrupts - Return v12 + Return v13 "); } @@ -17247,50 +17201,43 @@ mod hir_opt_tests { bb6(v19:HeapBasicObject, v20:Fixnum): v24:Fixnum[10] = Const Value(10) PatchPoint MethodRedefined(Integer@0x1000, <@0x1008, cme:0x1010) - v100:BoolExact = FixnumLt v20, v24 + v96:BoolExact = FixnumLt v20, v24 CheckInterrupts - v30:CBool = Test v100 + v30:CBool = Test v96 CondBranch v30, bb4(v19, v20), bb7() bb4(v40:HeapBasicObject, v41:Fixnum): PatchPoint SingleRactorMode - v48:CShape = LoadField v40, :shape_id@0x1038 + v47:CShape = LoadField v40, :shape_id@0x1038 v49:CShape[0x1039] = Const CShape(0x1039) - v50:CBool = IsBitEqual v48, v49 + v50:CBool = IsBitEqual v47, v49 CondBranch v50, bb9(), bb10() bb9(): v52:BasicObject = LoadField v40, :@levar@0x103a Jump bb8(v52) bb10(): - v54:CShape = LoadField v40, :shape_id@0x1038 - v55:CShape[0x103b] = Const CShape(0x103b) - v56:CBool = IsBitEqual v54, v55 - CondBranch v56, bb11(), bb12() - bb11(): - v58:NilClass = Const Value(nil) - Jump bb8(v58) - bb12(): - v60:BasicObject = GetIvar v40, :@levar - Jump bb8(v60) - bb8(v47:BasicObject): + v54:CShape[0x103b] = GuardBitEquals v47, CShape(0x103b) recompile + v56:NilClass = Const Value(nil) + Jump bb8(v56) + bb8(v48:BasicObject): CheckInterrupts - v64:CBool = Test v47 - CondBranch v64, bb5(v40, v41), bb13() - bb13(): + v60:CBool = Test v48 + CondBranch v60, bb5(v40, v41), bb12() + bb12(): PatchPoint NoEPEscape(set_value_loop) PatchPoint SingleRactorMode - v74:CShape = LoadField v40, :shape_id@0x1038 - v75:CShape[0x103b] = GuardBitEquals v74, CShape(0x103b) recompile + v70:CShape = LoadField v40, :shape_id@0x1038 + v71:CShape[0x103b] = GuardBitEquals v70, CShape(0x103b) recompile StoreField v40, :@levar@0x103a, v41 WriteBarrier v40, v41 - v78:CShape[0x1039] = Const CShape(0x1039) - StoreField v40, :shape_id@0x1038, v78 + v74:CShape[0x1039] = Const CShape(0x1039) + StoreField v40, :shape_id@0x1038, v74 Jump bb5(v40, v41) - bb5(v82:HeapBasicObject, v83:Fixnum): + bb5(v78:HeapBasicObject, v79:Fixnum): PatchPoint NoEPEscape(set_value_loop) - v90:Fixnum[1] = Const Value(1) + v86:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1000, +@0x103c, cme:0x1040) - v104:Fixnum = FixnumAdd v83, v90 - Jump bb6(v82, v104) + v100:Fixnum = FixnumAdd v79, v86 + Jump bb6(v78, v100) bb7(): v35:NilClass = Const Value(nil) CheckInterrupts diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 1ed38ad0c19305..e3271833326450 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -4826,15 +4826,8 @@ pub(crate) mod hir_build_tests { Jump bb3(v7, v8, v9, v10) bb3(v12:BasicObject, v13:NilClass, v14:NilClass, v15:NilClass): PatchPoint SingleRactorMode - v20:BasicObject = GetIvar v12, :@a - PatchPoint SingleRactorMode - v23:BasicObject = GetIvar v12, :@b - PatchPoint SingleRactorMode - v26:BasicObject = GetIvar v12, :@c - PatchPoint NoEPEscape(reverse_odd) - v38:ArrayExact = NewArray v20, v23, v26 - CheckInterrupts - Return v38 + v20:HeapBasicObject = GuardType v12, HeapBasicObject + SideExit NoProfileGetIvar recompile fn reverse_even@:8: bb1(): @@ -4855,17 +4848,8 @@ pub(crate) mod hir_build_tests { Jump bb3(v8, v9, v10, v11, v12) bb3(v14:BasicObject, v15:NilClass, v16:NilClass, v17:NilClass, v18:NilClass): PatchPoint SingleRactorMode - v23:BasicObject = GetIvar v14, :@a - PatchPoint SingleRactorMode - v26:BasicObject = GetIvar v14, :@b - PatchPoint SingleRactorMode - v29:BasicObject = GetIvar v14, :@c - PatchPoint SingleRactorMode - v32:BasicObject = GetIvar v14, :@d - PatchPoint NoEPEscape(reverse_even) - v46:ArrayExact = NewArray v23, v26, v29, v32 - CheckInterrupts - Return v46 + v23:HeapBasicObject = GuardType v14, HeapBasicObject + SideExit NoProfileGetIvar recompile "); } diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index ae8fd26c47b7b1..bd88b97eeed124 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -235,6 +235,7 @@ make_counters! { exit_too_many_keyword_parameters, exit_too_many_args_for_lir, exit_no_profile_send, + exit_no_profile_getivar, exit_splatkw_not_nil_or_hash, exit_splatkw_polymorphic, exit_splatkw_not_profiled, @@ -659,6 +660,7 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { => exit_patchpoint_root_box_only, SendWhileTracing => exit_send_while_tracing, NoProfileSend => exit_no_profile_send, + NoProfileGetIvar => exit_no_profile_getivar, InvokeBlockNotIfunc => exit_invokeblock_not_ifunc, } } From 0b4147c696e4d5f34de65a5772b36eb7ba42684d Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 8 Jul 2026 22:32:10 -0700 Subject: [PATCH 10/11] ZJIT: Return empty profile summary by default --- zjit/src/hir.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 89ee6c013fa00b..4f1b8750fa00e8 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -3496,17 +3496,17 @@ impl Function { } } - fn profile_summary(&self, profiles: &ProfileOracle, recv: InsnId, state: InsnId) -> Option { + fn profile_summary(&self, profiles: &ProfileOracle, recv: InsnId, state: InsnId) -> TypeDistributionSummary { let Some(entries) = profiles.get(state) else { - return None; + return TypeDistributionSummary::empty(); }; let recv = self.chase_insn(recv); for (entry_insn, entry_type_summary) in entries { if self.chase_insn(*entry_insn) == recv { - return Some(entry_type_summary.clone()); + return entry_type_summary.clone(); } } - None + TypeDistributionSummary::empty() } fn polymorphic_summary(&self, profiles: &ProfileOracle, recv: InsnId, state: InsnId) -> Option { @@ -8993,9 +8993,7 @@ fn add_iseq_to_hir( fun.push_insn(block, Insn::SideExit { state: exit_id, reason: Box::new(SideExitReason::UnhandledYARVInsn(opcode)), recompile: None }); break; // End the block } - let summary = fun.profile_summary(&profiles, self_param, exit_id).unwrap_or_else(|| { - TypeDistributionSummary::empty() - }); + let summary = fun.profile_summary(&profiles, self_param, exit_id); let self_param = fun.guard_heap(block, self_param, exit_id); // Filter out profiled types we don't care to optimize let profiled_types = summary.buckets().iter().filter(|profiled_type| { From 9c3275a5174015ec166bb0231bf0cd038b3f0964 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 9 Jul 2026 10:58:17 -0700 Subject: [PATCH 11/11] ZJIT Add missing compile-time counters to stats output (#17757) We were missing a couple. --- zjit.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zjit.rb b/zjit.rb index bff9cfe1538621..22eba6e1475260 100644 --- a/zjit.rb +++ b/zjit.rb @@ -152,9 +152,12 @@ def stats_string :compile_hir_build_time_ns, :compile_hir_strength_reduce_time_ns, :compile_hir_inline_methods_time_ns, + :compile_hir_optimize_load_store_time_ns, :compile_hir_canonicalize_time_ns, :compile_hir_fold_constants_time_ns, :compile_hir_clean_cfg_time_ns, + :compile_hir_remove_redundant_patch_points_time_ns, + :compile_hir_remove_duplicate_check_interrupts_time_ns, :compile_hir_eliminate_dead_code_time_ns, :compile_lir_time_ns, :profile_time_ns,