From 7ae0809c7c03b9d31a57fb18e9b0d173eead6f74 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 21 Nov 2025 10:00:39 -0500 Subject: [PATCH 01/14] Remove clang-18 from compilers CI clang-18 has a bug that causes ruby_current_ec to sometimes be null when using Ractors and crashes like this: :700: [BUG] Segmentation fault at 0x0000000000000030 ruby 4.0.0dev (2025-11-21T06:49:14Z master bcc7b2049c) +PRISM [x86_64-linux] -- Control frame information ----------------------------------------------- c:0004 p:0003 s:0015 e:000014 l:y b:0001 METHOD :700 me: called_id: receive, type: iseq owner class: 0x00007ff462dda500 T_CLASS/Ractor::Port self: 0x00007ff46146d068 ractor/port/Ractor::Port ractor/port c:0003 p:0008 s:0011 e:000010 l:y b:0001 METHOD :311 me: called_id: receive, type: iseq owner class: 0x00007ff462ddae60 T_CLASS/(anon) self: 0x00007ff462ddaf00 T_CLASS/Ractor c:0002 p:0010 s:0007 e:000006 l:n b:---- BLOCK bootstraptest.test_ractor.rb_2354_1323.rb:9 [FINISH] self: 0x00007ff46146d090 ractor/Ractor r:68 lvars: j: T_FIXNUM 66 c:0001 p:---- s:0003 e:000002 l:y b:---- DUMMY [FINISH] self: T_NIL -- Ruby level backtrace information ---------------------------------------- bootstraptest.test_ractor.rb_2354_1323.rb:9:in 'block (2 levels) in
' :311:in 'receive' :700:in 'receive' -- Threading information --------------------------------------------------- Total ractor count: 7 Ruby thread count for this ractor: 1 -- Machine register context ------------------------------------------------ RIP: 0x00007ff47c7df5f0 RBP: 0x000055d77ea5b4f0 RSP: 0x00007ff445fa3af0 RAX: 0x0000000000000000 RBX: 0x000055d77e9fd068 RCX: 0x000055d77e9fd040 RDX: 0x000055d77eb2ac40 RDI: 0x00007ff47cbe7700 RSI: 0x0000000000000000 R8: 0x0000000000000000 R9: 0x0000000000000000 R10: 0x000055d77e9fc830 R11: 0x93ba1054e59bfb14 R12: 0x000055d77ea5b4f0 R13: 0x00007ff445f82f20 R14: 0x00007ff4614cf668 R15: 0x000055d77e9fd040 EFL: 0x0000000000010246 -- C level backtrace information ------------------------------------------- libruby.so.4.0(rb_print_backtrace+0x14) [0x7ff47c8cbd18] vm_dump.c:1105 libruby.so.4.0(rb_vm_bugreport) vm_dump.c:1450 libruby.so.4.0(rb_bug_for_fatal_signal+0x162) [0x7ff47c70ce02] error.c:1131 libruby.so.4.0(sigsegv+0x4a) [0x7ff47c82f20a] signal.c:948 /lib/x86_64-linux-gnu/libc.so.6(0x7ff47c34a330) [0x7ff47c34a330] libruby.so.4.0(rb_ec_thread_ptr+0x0) [0x7ff47c7df5f0] vm_core.h:2092 libruby.so.4.0(rb_ec_ractor_ptr) vm_core.h:2041 libruby.so.4.0(rb_current_execution_context) vm_core.h:2110 libruby.so.4.0(rb_current_ractor_raw) vm_core.h:2109 libruby.so.4.0(rb_current_ractor) vm_core.h:2117 libruby.so.4.0(ractor_unlock) ractor.c:110 libruby.so.4.0(ractor_unlock_self) ractor.c:125 libruby.so.4.0(ractor_wait) ractor_sync.c:1054 libruby.so.4.0(ractor_wait_receive) ractor_sync.c:1113 libruby.so.4.0(ractor_receive+0x25) [0x7ff47c7ded08] ractor_sync.c:1166 libruby.so.4.0(ractor_port_receive) ractor_sync.c:143 libruby.so.4.0(builtin_inline_class_700) ractor.rb:701 libruby.so.4.0(invoke_bf+0x4) [0x7ff47c8a2060] vm_insnhelper.c:7534 libruby.so.4.0(vm_invoke_builtin_delegate) vm_insnhelper.c:0 libruby.so.4.0(vm_exec_core) insns.def:1674 libruby.so.4.0(vm_exec_loop+0x0) [0x7ff47c89b868] vm.c:2784 libruby.so.4.0(rb_vm_exec) vm.c:2787 libruby.so.4.0(vm_invoke_proc+0x344) [0x7ff47c8b03f4] vm.c:1814 libruby.so.4.0(thread_do_start_proc+0x17a) [0x7ff47c870bba] thread.c:593 libruby.so.4.0(thread_do_start+0x162) [0x7ff47c87042f] thread.c:635 libruby.so.4.0(thread_start_func_2) thread.c:686 libruby.so.4.0(rb_native_mutex_lock+0x0) [0x7ff47c870fd1] thread_pthread.c:2238 libruby.so.4.0(thread_sched_lock_) thread_pthread.c:403 libruby.so.4.0(call_thread_start_func_2) thread_pthread_mn.c:466 libruby.so.4.0(co_start) thread_pthread_mn.c:464 --- .github/workflows/compilers.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/compilers.yml b/.github/workflows/compilers.yml index 8c73729a5c9bfe..ffddbe81e3b263 100644 --- a/.github/workflows/compilers.yml +++ b/.github/workflows/compilers.yml @@ -109,7 +109,8 @@ jobs: - { uses: './.github/actions/compilers', name: 'clang 21', with: { tag: 'clang-21' } } - { uses: './.github/actions/compilers', name: 'clang 20', with: { tag: 'clang-20' } } - { uses: './.github/actions/compilers', name: 'clang 19', with: { tag: 'clang-19' } } - - { uses: './.github/actions/compilers', name: 'clang 18', with: { tag: 'clang-18' } } + # clang-18 has a bug causing ruby_current_ec to sometimes be null + # - { uses: './.github/actions/compilers', name: 'clang 18', with: { tag: 'clang-18' } } - { uses: './.github/actions/compilers', name: 'clang 17', with: { tag: 'clang-17' } } - { uses: './.github/actions/compilers', name: 'clang 16', with: { tag: 'clang-16' } } - { uses: './.github/actions/compilers', name: 'clang 15', with: { tag: 'clang-15' } } From bdca2a9975c7859f2e1702a517d59bb6cb254acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Tue, 18 Nov 2025 11:44:06 +0100 Subject: [PATCH 02/14] [ruby/json] Ractor-shareable JSON::Coder https://github.com/ruby/json/commit/58d60d6b76 --- ext/json/generator/generator.c | 14 ++++++++ ext/json/lib/json/common.rb | 2 +- ext/json/parser/parser.c | 8 ++++- test/json/json_generator_test.rb | 14 ++++++++ test/json/json_parser_test.rb | 7 ++++ test/json/ractor_test.rb | 59 ++++++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+), 2 deletions(-) diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index fb8424dd86d736..2fb00ee2787539 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -1630,6 +1630,7 @@ static VALUE string_config(VALUE config) */ static VALUE cState_indent_set(VALUE self, VALUE indent) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->indent, string_config(indent)); return Qnil; @@ -1655,6 +1656,7 @@ static VALUE cState_space(VALUE self) */ static VALUE cState_space_set(VALUE self, VALUE space) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->space, string_config(space)); return Qnil; @@ -1678,6 +1680,7 @@ static VALUE cState_space_before(VALUE self) */ static VALUE cState_space_before_set(VALUE self, VALUE space_before) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->space_before, string_config(space_before)); return Qnil; @@ -1703,6 +1706,7 @@ static VALUE cState_object_nl(VALUE self) */ static VALUE cState_object_nl_set(VALUE self, VALUE object_nl) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->object_nl, string_config(object_nl)); return Qnil; @@ -1726,6 +1730,7 @@ static VALUE cState_array_nl(VALUE self) */ static VALUE cState_array_nl_set(VALUE self, VALUE array_nl) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->array_nl, string_config(array_nl)); return Qnil; @@ -1749,6 +1754,7 @@ static VALUE cState_as_json(VALUE self) */ static VALUE cState_as_json_set(VALUE self, VALUE as_json) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->as_json, rb_convert_type(as_json, T_DATA, "Proc", "to_proc")); return Qnil; @@ -1791,6 +1797,7 @@ static long long_config(VALUE num) */ static VALUE cState_max_nesting_set(VALUE self, VALUE depth) { + rb_check_frozen(self); GET_STATE(self); state->max_nesting = long_config(depth); return Qnil; @@ -1816,6 +1823,7 @@ static VALUE cState_script_safe(VALUE self) */ static VALUE cState_script_safe_set(VALUE self, VALUE enable) { + rb_check_frozen(self); GET_STATE(self); state->script_safe = RTEST(enable); return Qnil; @@ -1847,6 +1855,7 @@ static VALUE cState_strict(VALUE self) */ static VALUE cState_strict_set(VALUE self, VALUE enable) { + rb_check_frozen(self); GET_STATE(self); state->strict = RTEST(enable); return Qnil; @@ -1871,6 +1880,7 @@ static VALUE cState_allow_nan_p(VALUE self) */ static VALUE cState_allow_nan_set(VALUE self, VALUE enable) { + rb_check_frozen(self); GET_STATE(self); state->allow_nan = RTEST(enable); return Qnil; @@ -1895,6 +1905,7 @@ static VALUE cState_ascii_only_p(VALUE self) */ static VALUE cState_ascii_only_set(VALUE self, VALUE enable) { + rb_check_frozen(self); GET_STATE(self); state->ascii_only = RTEST(enable); return Qnil; @@ -1932,6 +1943,7 @@ static VALUE cState_depth(VALUE self) */ static VALUE cState_depth_set(VALUE self, VALUE depth) { + rb_check_frozen(self); GET_STATE(self); state->depth = long_config(depth); return Qnil; @@ -1965,6 +1977,7 @@ static void buffer_initial_length_set(JSON_Generator_State *state, VALUE buffer_ */ static VALUE cState_buffer_initial_length_set(VALUE self, VALUE buffer_initial_length) { + rb_check_frozen(self); GET_STATE(self); buffer_initial_length_set(state, buffer_initial_length); return Qnil; @@ -2031,6 +2044,7 @@ static void configure_state(JSON_Generator_State *state, VALUE vstate, VALUE con static VALUE cState_configure(VALUE self, VALUE opts) { + rb_check_frozen(self); GET_STATE(self); configure_state(state, self, opts); return self; diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb index f22d911f552ec5..233b8c7e62d628 100644 --- a/ext/json/lib/json/common.rb +++ b/ext/json/lib/json/common.rb @@ -1048,7 +1048,7 @@ def initialize(options = nil, &as_json) options[:as_json] = as_json if as_json @state = State.new(options).freeze - @parser_config = Ext::Parser::Config.new(ParserOptions.prepare(options)) + @parser_config = Ext::Parser::Config.new(ParserOptions.prepare(options)).freeze end # call-seq: diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index a1d0265a919370..e23945d0e56fb3 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -1466,6 +1466,7 @@ static void parser_config_init(JSON_ParserConfig *config, VALUE opts) */ static VALUE cParserConfig_initialize(VALUE self, VALUE opts) { + rb_check_frozen(self); GET_PARSER_CONFIG; parser_config_init(config, opts); @@ -1553,6 +1554,11 @@ static size_t JSON_ParserConfig_memsize(const void *ptr) return sizeof(JSON_ParserConfig); } +#ifndef HAVE_RB_EXT_RACTOR_SAFE +# undef RUBY_TYPED_FROZEN_SHAREABLE +# define RUBY_TYPED_FROZEN_SHAREABLE 0 +#endif + static const rb_data_type_t JSON_ParserConfig_type = { "JSON::Ext::Parser/ParserConfig", { @@ -1561,7 +1567,7 @@ static const rb_data_type_t JSON_ParserConfig_type = { JSON_ParserConfig_memsize, }, 0, 0, - RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, + RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FROZEN_SHAREABLE, }; static VALUE cJSON_parser_s_allocate(VALUE klass) diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 1b3c702c982f94..54a2ec61409eeb 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -901,4 +901,18 @@ def test_generate_duplicate_keys_disallowed end assert_equal %(detected duplicate key "foo" in #{hash.inspect}), error.message end + + def test_frozen + state = JSON::State.new.freeze + assert_raise(FrozenError) do + state.configure(max_nesting: 1) + end + setters = state.methods.grep(/\w=$/) + assert_not_empty setters + setters.each do |setter| + assert_raise(FrozenError) do + state.send(setter, 1) + end + end + end end diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 6315c3e667be85..544a8b564f670c 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -820,6 +820,13 @@ def test_parse_whitespace_after_newline assert_equal [], JSON.parse("[\n#{' ' * (8 + 8 + 4 + 3)}]") end + def test_frozen + parser_config = JSON::Parser::Config.new({}).freeze + assert_raise FrozenError do + parser_config.send(:initialize, {}) + end + end + private def assert_equal_float(expected, actual, delta = 1e-2) diff --git a/test/json/ractor_test.rb b/test/json/ractor_test.rb index 53e1099ce9b76c..e53c405a74440f 100644 --- a/test/json/ractor_test.rb +++ b/test/json/ractor_test.rb @@ -52,4 +52,63 @@ def test_generate _, status = Process.waitpid2(pid) assert_predicate status, :success? end + + def test_coder + coder = JSON::Coder.new.freeze + assert Ractor.shareable?(coder) + pid = fork do + Warning[:experimental] = false + r = Ractor.new(coder) do |coder| + json = coder.dump({ + 'a' => 2, + 'b' => 3.141, + 'c' => 'c', + 'd' => [ 1, "b", 3.14 ], + 'e' => { 'foo' => 'bar' }, + 'g' => "\"\0\037", + 'h' => 1000.0, + 'i' => 0.001 + }) + coder.load(json) + end + expected_json = JSON.parse('{"a":2,"b":3.141,"c":"c","d":[1,"b",3.14],"e":{"foo":"bar"},' + + '"g":"\\"\\u0000\\u001f","h":1000.0,"i":0.001}') + actual_json = r.value + + if expected_json == actual_json + exit 0 + else + puts "Expected:" + puts expected_json + puts "Actual:" + puts actual_json + puts + exit 1 + end + end + _, status = Process.waitpid2(pid) + assert_predicate status, :success? + end + + class NonNative + def initialize(value) + @value = value + end + end + + def test_coder_proc + block = Ractor.shareable_proc { |value| value.as_json } + coder = JSON::Coder.new(&block).freeze + assert Ractor.shareable?(coder) + + pid = fork do + Warning[:experimental] = false + assert_equal [{}], Ractor.new(coder) { |coder| + coder.load('[{}]') + }.value + end + + _, status = Process.waitpid2(pid) + assert_predicate status, :success? + end if Ractor.respond_to?(:shareable_proc) end if defined?(Ractor) && Process.respond_to?(:fork) From 419efd5ca5064e8f492660a898be36fe4f79b84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Fri, 21 Nov 2025 11:53:17 +0100 Subject: [PATCH 03/14] [ruby/json] Skip test failing with JRuby in CI https://github.com/ruby/json/commit/305d3832db --- test/json/json_parser_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 544a8b564f670c..a5b4763618c192 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -822,6 +822,7 @@ def test_parse_whitespace_after_newline def test_frozen parser_config = JSON::Parser::Config.new({}).freeze + omit "JRuby failure in CI" if RUBY_ENGINE == "jruby" assert_raise FrozenError do parser_config.send(:initialize, {}) end From ffa105c27f943bf4170247137733ff7640cf24d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Fri, 21 Nov 2025 12:48:21 +0100 Subject: [PATCH 04/14] [ruby/json] Move RUBY_TYPED_FROZEN_SHAREABLE macro to json.h https://github.com/ruby/json/commit/2a4ebe8250 --- ext/json/generator/generator.c | 5 ----- ext/json/json.h | 5 +++++ ext/json/parser/parser.c | 5 ----- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index 2fb00ee2787539..8d04bef53f1638 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -926,11 +926,6 @@ static size_t State_memsize(const void *ptr) return sizeof(JSON_Generator_State); } -#ifndef HAVE_RB_EXT_RACTOR_SAFE -# undef RUBY_TYPED_FROZEN_SHAREABLE -# define RUBY_TYPED_FROZEN_SHAREABLE 0 -#endif - static const rb_data_type_t JSON_Generator_State_type = { "JSON/Generator/State", { diff --git a/ext/json/json.h b/ext/json/json.h index 01fe0cd034e7e3..28efa04c257a94 100644 --- a/ext/json/json.h +++ b/ext/json/json.h @@ -45,6 +45,11 @@ typedef unsigned char _Bool; #endif #endif +#ifndef HAVE_RB_EXT_RACTOR_SAFE +# undef RUBY_TYPED_FROZEN_SHAREABLE +# define RUBY_TYPED_FROZEN_SHAREABLE 0 +#endif + #ifndef NORETURN #define NORETURN(x) x #endif diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index e23945d0e56fb3..2dcd1012b1270f 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -1554,11 +1554,6 @@ static size_t JSON_ParserConfig_memsize(const void *ptr) return sizeof(JSON_ParserConfig); } -#ifndef HAVE_RB_EXT_RACTOR_SAFE -# undef RUBY_TYPED_FROZEN_SHAREABLE -# define RUBY_TYPED_FROZEN_SHAREABLE 0 -#endif - static const rb_data_type_t JSON_ParserConfig_type = { "JSON::Ext::Parser/ParserConfig", { From d3b6f835d565ec1590059773fc87589ddf8adc37 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 22 Nov 2025 00:54:58 +0900 Subject: [PATCH 05/14] Fix stdatomic case in `rbimpl_atomic_u64_fetch_add` On some platoforms, 64bit atomic operations need the dedicated helper library. --- configure.ac | 1 + ruby_atomic.h | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 339ee3b2f2e66e..1e406ec56cff8a 100644 --- a/configure.ac +++ b/configure.ac @@ -1746,6 +1746,7 @@ AS_IF([test "$GCC" = yes], [ [rb_cv_gcc_atomic_builtins=no])]) AS_IF([test "$rb_cv_gcc_atomic_builtins" = yes], [ AC_DEFINE(HAVE_GCC_ATOMIC_BUILTINS) + AC_CHECK_LIB([atomic], [__atomic_fetch_add_8]) AC_CACHE_CHECK([for 64bit __atomic builtins], [rb_cv_gcc_atomic_builtins_64], [ AC_LINK_IFELSE([AC_LANG_PROGRAM([[@%:@include uint64_t atomic_var;]], diff --git a/ruby_atomic.h b/ruby_atomic.h index c194f7ec3b82fc..3a541d92082824 100644 --- a/ruby_atomic.h +++ b/ruby_atomic.h @@ -2,6 +2,9 @@ #define INTERNAL_ATOMIC_H #include "ruby/atomic.h" +#ifdef HAVE_STDATOMIC_H +# include +#endif #define RUBY_ATOMIC_VALUE_LOAD(x) rbimpl_atomic_value_load(&(x), RBIMPL_ATOMIC_SEQ_CST) @@ -76,9 +79,9 @@ rbimpl_atomic_u64_fetch_add(volatile rbimpl_atomic_uint64_t *ptr, uint64_t val) return InterlockedExchangeAdd64((volatile LONG64 *)ptr, val); #elif defined(__sun) && defined(HAVE_ATOMIC_H) && (defined(_LP64) || defined(_I32LPx)) return atomic_add_64_nv(ptr, val) - val; +#elif defined(HAVE_STDATOMIC_H) + return atomic_fetch_add_explicit((_Atomic uint64_t *)ptr, val, memory_order_seq_cst); #else - // TODO: stdatomic - // Fallback using mutex for platforms without 64-bit atomics static rb_native_mutex_t lock = RB_NATIVE_MUTEX_INITIALIZER; rb_native_mutex_lock(&lock); From e5e8ac51496d8240f2c7a65aa9a9f300454d41b6 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 21 Nov 2025 08:48:18 -0800 Subject: [PATCH 06/14] ZJIT: Inline String#empty? (#15283) Don't emit a CCall. --- zjit/src/cruby_methods.rs | 16 +++++++++++++++- zjit/src/hir.rs | 2 ++ zjit/src/hir/opt_tests.rs | 7 +++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 7a4a11a8e18bfd..190ac52eaceccb 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -200,7 +200,7 @@ pub fn init() -> Annotations { annotate!(rb_cString, "length", types::Fixnum, no_gc, leaf, elidable); annotate!(rb_cString, "getbyte", inline_string_getbyte); annotate!(rb_cString, "setbyte", inline_string_setbyte); - annotate!(rb_cString, "empty?", types::BoolExact, no_gc, leaf, elidable); + annotate!(rb_cString, "empty?", inline_string_empty_p, types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cString, "<<", inline_string_append); annotate!(rb_cString, "==", inline_string_eq); annotate!(rb_cModule, "name", types::StringExact.union(types::NilClass), no_gc, leaf, elidable); @@ -376,6 +376,20 @@ 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: ID!(len), + offset: RUBY_OFFSET_RSTRING_LEN as i32, + return_type: types::CInt64, + }); + 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 }); + Some(result) +} + fn inline_string_append(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { let &[other] = args else { return None; }; // Inline only StringExact << String, which matches original type check from diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 961fe1c1428825..3866da52b995ed 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -992,6 +992,8 @@ impl Insn { Insn::StringGetbyteFixnum { .. } => false, Insn::IsBlockGiven => false, Insn::BoxFixnum { .. } => false, + Insn::BoxBool { .. } => false, + Insn::IsBitEqual { .. } => false, _ => true, } } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 70efa000761acf..083820c0da3450 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6263,10 +6263,13 @@ mod hir_opt_tests { PatchPoint MethodRedefined(String@0x1000, empty?@0x1008, cme:0x1010) PatchPoint NoSingletonClass(String@0x1000) v23:StringExact = GuardType v9, StringExact + v24:CInt64 = LoadField v23, :len@0x1038 + v25:CInt64[0] = Const CInt64(0) + v26:CBool = IsBitEqual v24, v25 + v27:BoolExact = BoxBool v26 IncrCounter inline_cfunc_optimized_send_count - v25:BoolExact = CCall String#empty?@0x1038, v23 CheckInterrupts - Return v25 + Return v27 "); } From f52edf172db0afa4b3867723f75d617291070d63 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 21 Nov 2025 08:48:36 -0800 Subject: [PATCH 07/14] ZJIT: Specialize monomorphic DefinedIvar (#15281) This lets us constant-fold common monomorphic cases. --- insns.def | 1 + zjit/src/cruby_bindings.inc.rs | 57 +++++++++--------- zjit/src/hir.rs | 34 +++++++++++ zjit/src/hir/opt_tests.rs | 107 +++++++++++++++++++++++++++++++++ zjit/src/profile.rs | 1 + 5 files changed, 172 insertions(+), 28 deletions(-) diff --git a/insns.def b/insns.def index f282b5a8e7084c..3d13f4cb646d39 100644 --- a/insns.def +++ b/insns.def @@ -745,6 +745,7 @@ definedivar () (VALUE val) // attr bool leaf = false; +// attr bool zjit_profile = true; { val = Qnil; if (!UNDEF_P(vm_getivar(GET_SELF(), id, GET_ISEQ(), ic, NULL, FALSE, Qundef))) { diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 66126b627c0fd3..fe2055d4cc0486 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1744,34 +1744,35 @@ pub const YARVINSN_trace_setlocal_WC_1: ruby_vminsn_type = 215; pub const YARVINSN_trace_putobject_INT2FIX_0_: ruby_vminsn_type = 216; pub const YARVINSN_trace_putobject_INT2FIX_1_: ruby_vminsn_type = 217; pub const YARVINSN_zjit_getinstancevariable: ruby_vminsn_type = 218; -pub const YARVINSN_zjit_send: ruby_vminsn_type = 219; -pub const YARVINSN_zjit_opt_send_without_block: ruby_vminsn_type = 220; -pub const YARVINSN_zjit_objtostring: ruby_vminsn_type = 221; -pub const YARVINSN_zjit_opt_nil_p: ruby_vminsn_type = 222; -pub const YARVINSN_zjit_invokeblock: ruby_vminsn_type = 223; -pub const YARVINSN_zjit_opt_plus: ruby_vminsn_type = 224; -pub const YARVINSN_zjit_opt_minus: ruby_vminsn_type = 225; -pub const YARVINSN_zjit_opt_mult: ruby_vminsn_type = 226; -pub const YARVINSN_zjit_opt_div: ruby_vminsn_type = 227; -pub const YARVINSN_zjit_opt_mod: ruby_vminsn_type = 228; -pub const YARVINSN_zjit_opt_eq: ruby_vminsn_type = 229; -pub const YARVINSN_zjit_opt_neq: ruby_vminsn_type = 230; -pub const YARVINSN_zjit_opt_lt: ruby_vminsn_type = 231; -pub const YARVINSN_zjit_opt_le: ruby_vminsn_type = 232; -pub const YARVINSN_zjit_opt_gt: ruby_vminsn_type = 233; -pub const YARVINSN_zjit_opt_ge: ruby_vminsn_type = 234; -pub const YARVINSN_zjit_opt_ltlt: ruby_vminsn_type = 235; -pub const YARVINSN_zjit_opt_and: ruby_vminsn_type = 236; -pub const YARVINSN_zjit_opt_or: ruby_vminsn_type = 237; -pub const YARVINSN_zjit_opt_aref: ruby_vminsn_type = 238; -pub const YARVINSN_zjit_opt_aset: ruby_vminsn_type = 239; -pub const YARVINSN_zjit_opt_length: ruby_vminsn_type = 240; -pub const YARVINSN_zjit_opt_size: ruby_vminsn_type = 241; -pub const YARVINSN_zjit_opt_empty_p: ruby_vminsn_type = 242; -pub const YARVINSN_zjit_opt_succ: ruby_vminsn_type = 243; -pub const YARVINSN_zjit_opt_not: ruby_vminsn_type = 244; -pub const YARVINSN_zjit_opt_regexpmatch2: ruby_vminsn_type = 245; -pub const VM_INSTRUCTION_SIZE: ruby_vminsn_type = 246; +pub const YARVINSN_zjit_definedivar: ruby_vminsn_type = 219; +pub const YARVINSN_zjit_send: ruby_vminsn_type = 220; +pub const YARVINSN_zjit_opt_send_without_block: ruby_vminsn_type = 221; +pub const YARVINSN_zjit_objtostring: ruby_vminsn_type = 222; +pub const YARVINSN_zjit_opt_nil_p: ruby_vminsn_type = 223; +pub const YARVINSN_zjit_invokeblock: ruby_vminsn_type = 224; +pub const YARVINSN_zjit_opt_plus: ruby_vminsn_type = 225; +pub const YARVINSN_zjit_opt_minus: ruby_vminsn_type = 226; +pub const YARVINSN_zjit_opt_mult: ruby_vminsn_type = 227; +pub const YARVINSN_zjit_opt_div: ruby_vminsn_type = 228; +pub const YARVINSN_zjit_opt_mod: ruby_vminsn_type = 229; +pub const YARVINSN_zjit_opt_eq: ruby_vminsn_type = 230; +pub const YARVINSN_zjit_opt_neq: ruby_vminsn_type = 231; +pub const YARVINSN_zjit_opt_lt: ruby_vminsn_type = 232; +pub const YARVINSN_zjit_opt_le: ruby_vminsn_type = 233; +pub const YARVINSN_zjit_opt_gt: ruby_vminsn_type = 234; +pub const YARVINSN_zjit_opt_ge: ruby_vminsn_type = 235; +pub const YARVINSN_zjit_opt_ltlt: ruby_vminsn_type = 236; +pub const YARVINSN_zjit_opt_and: ruby_vminsn_type = 237; +pub const YARVINSN_zjit_opt_or: ruby_vminsn_type = 238; +pub const YARVINSN_zjit_opt_aref: ruby_vminsn_type = 239; +pub const YARVINSN_zjit_opt_aset: ruby_vminsn_type = 240; +pub const YARVINSN_zjit_opt_length: ruby_vminsn_type = 241; +pub const YARVINSN_zjit_opt_size: ruby_vminsn_type = 242; +pub const YARVINSN_zjit_opt_empty_p: ruby_vminsn_type = 243; +pub const YARVINSN_zjit_opt_succ: ruby_vminsn_type = 244; +pub const YARVINSN_zjit_opt_not: ruby_vminsn_type = 245; +pub const YARVINSN_zjit_opt_regexpmatch2: ruby_vminsn_type = 246; +pub const VM_INSTRUCTION_SIZE: ruby_vminsn_type = 247; pub type ruby_vminsn_type = u32; pub type rb_iseq_callback = ::std::option::Option< unsafe extern "C" fn(arg1: *const rb_iseq_t, arg2: *mut ::std::os::raw::c_void), diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 3866da52b995ed..40c6092e56c471 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2820,6 +2820,38 @@ impl Function { }; self.make_equal_to(insn_id, replacement); } + Insn::DefinedIvar { self_val, id, pushval, state } => { + let frame_state = self.frame_state(state); + let Some(recv_type) = self.profiled_type_of_at(self_val, frame_state.insn_idx) else { + // No (monomorphic/skewed polymorphic) profile info + self.push_insn_id(block, insn_id); continue; + }; + if recv_type.flags().is_immediate() { + // Instance variable lookups on immediate values are always nil + self.push_insn_id(block, insn_id); continue; + } + assert!(recv_type.shape().is_valid()); + if !recv_type.flags().is_t_object() { + // Check if the receiver is a T_OBJECT + self.push_insn_id(block, insn_id); continue; + } + if recv_type.shape().is_too_complex() { + // too-complex shapes can't use index access + self.push_insn_id(block, insn_id); continue; + } + let self_val = self.push_insn(block, Insn::GuardType { val: self_val, guard_type: types::HeapBasicObject, state }); + let _ = self.push_insn(block, Insn::GuardShape { val: self_val, shape: recv_type.shape(), state }); + let mut ivar_index: u16 = 0; + let replacement = if unsafe { rb_shape_get_iv_index(recv_type.shape().0, id, &mut ivar_index) } { + self.push_insn(block, Insn::Const { val: Const::Value(pushval) }) + } else { + // If there is no IVAR index, then the ivar was undefined when we + // entered the compiler. That means we can just return nil for this + // shape + iv name + self.push_insn(block, Insn::Const { val: Const::Value(Qnil) }) + }; + self.make_equal_to(insn_id, replacement); + } _ => { self.push_insn_id(block, insn_id); } } } @@ -4839,6 +4871,8 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // profiled cfp->self. if opcode == YARVINSN_getinstancevariable || opcode == YARVINSN_trace_getinstancevariable { profiles.profile_self(&exit_state, self_param); + } else if opcode == YARVINSN_definedivar || opcode == YARVINSN_trace_definedivar { + profiles.profile_self(&exit_state, self_param); } else if opcode == YARVINSN_invokeblock || opcode == YARVINSN_trace_invokeblock { if get_option!(stats) { let iseq_insn_idx = exit_state.insn_idx; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 083820c0da3450..b1ab8a06050d82 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -3373,6 +3373,113 @@ mod hir_opt_tests { "); } + #[test] + fn test_specialize_monomorphic_definedivar_true() { + eval(" + @foo = 4 + def test = defined?(@foo) + test + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v15:HeapBasicObject = GuardType v6, HeapBasicObject + v16:HeapBasicObject = GuardShape v15, 0x1000 + v17:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + CheckInterrupts + Return v17 + "); + } + + #[test] + fn test_specialize_monomorphic_definedivar_false() { + eval(" + def test = defined?(@foo) + test + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v15:HeapBasicObject = GuardType v6, HeapBasicObject + v16:HeapBasicObject = GuardShape v15, 0x1000 + v17:NilClass = Const Value(nil) + CheckInterrupts + Return v17 + "); + } + + #[test] + fn test_dont_specialize_definedivar_with_t_data() { + eval(" + class C < Range + def test = defined?(@a) + end + obj = C.new 0, 1 + obj.instance_variable_set(:@a, 1) + obj.test + TEST = C.instance_method(:test) + "); + assert_snapshot!(hir_string_proc("TEST"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:StringExact|NilClass = DefinedIvar v6, :@a + CheckInterrupts + Return v10 + "); + } + + #[test] + fn test_dont_specialize_polymorphic_definedivar() { + set_call_threshold(3); + eval(" + class C + def test = defined?(@a) + end + obj = C.new + obj.instance_variable_set(:@a, 1) + obj.test + obj = C.new + obj.instance_variable_set(:@b, 1) + obj.test + TEST = C.instance_method(:test) + "); + assert_snapshot!(hir_string_proc("TEST"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:StringExact|NilClass = DefinedIvar v6, :@a + CheckInterrupts + Return v10 + "); + } + #[test] fn test_elide_freeze_with_frozen_hash() { eval(" diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index 8c8190609d7fae..10afdf2cc6475c 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -82,6 +82,7 @@ fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { YARVINSN_opt_aset => profile_operands(profiler, profile, 3), YARVINSN_opt_not => profile_operands(profiler, profile, 1), YARVINSN_getinstancevariable => profile_self(profiler, profile), + YARVINSN_definedivar => profile_self(profiler, profile), YARVINSN_opt_regexpmatch2 => profile_operands(profiler, profile, 2), YARVINSN_objtostring => profile_operands(profiler, profile, 1), YARVINSN_opt_length => profile_operands(profiler, profile, 1), From 6cebbf4037376f28d9792cecf38d4f770bcdcaac Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 20 Nov 2025 10:43:08 -0500 Subject: [PATCH 08/14] ZJIT: Split CSel memory reads on x86_64 Fix https://github.com/Shopify/ruby/issues/876 --- zjit/src/backend/x86_64/mod.rs | 44 +++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/zjit/src/backend/x86_64/mod.rs b/zjit/src/backend/x86_64/mod.rs index 11876eb89437a6..d17286f51fda25 100644 --- a/zjit/src/backend/x86_64/mod.rs +++ b/zjit/src/backend/x86_64/mod.rs @@ -542,14 +542,23 @@ impl Assembler { *opnds = vec![]; asm.push_insn(insn); } - Insn::CSelZ { out, .. } | - Insn::CSelNZ { out, .. } | - Insn::CSelE { out, .. } | - Insn::CSelNE { out, .. } | - Insn::CSelL { out, .. } | - Insn::CSelLE { out, .. } | - Insn::CSelG { out, .. } | - Insn::CSelGE { out, .. } | + Insn::CSelZ { truthy: left, falsy: right, out } | + Insn::CSelNZ { truthy: left, falsy: right, out } | + Insn::CSelE { truthy: left, falsy: right, out } | + Insn::CSelNE { truthy: left, falsy: right, out } | + Insn::CSelL { truthy: left, falsy: right, out } | + Insn::CSelLE { truthy: left, falsy: right, out } | + Insn::CSelG { truthy: left, falsy: right, out } | + Insn::CSelGE { truthy: left, falsy: right, out } => { + *left = split_stack_membase(asm, *left, SCRATCH1_OPND, &stack_state); + *right = split_stack_membase(asm, *right, SCRATCH0_OPND, &stack_state); + *right = split_if_both_memory(asm, *right, *left, SCRATCH0_OPND); + let mem_out = split_memory_write(out, SCRATCH0_OPND); + asm.push_insn(insn); + if let Some(mem_out) = mem_out { + asm.store(mem_out, SCRATCH0_OPND); + } + } Insn::Lea { out, .. } => { let mem_out = split_memory_write(out, SCRATCH0_OPND); asm.push_insn(insn); @@ -1776,4 +1785,23 @@ mod tests { "); assert_snapshot!(cb.hexdump(), @"49bb00100000000000004c891b"); } + + #[test] + fn test_csel_split_memory_read() { + let (mut asm, mut cb) = setup_asm(); + + let left = Opnd::Mem(Mem { base: MemBase::Stack { stack_idx: 0, num_bits: 64 }, disp: 0, num_bits: 64 }); + let right = Opnd::Mem(Mem { base: MemBase::Stack { stack_idx: 1, num_bits: 64 }, disp: 2, num_bits: 64 }); + let _ = asm.csel_e(left, right); + asm.compile_with_num_regs(&mut cb, 0); + + assert_disasm_snapshot!(cb.disasm(), @" + 0x0: mov r10, qword ptr [rbp - 8] + 0x4: mov r11, qword ptr [rbp - 0x10] + 0x8: mov r11, qword ptr [r11 + 2] + 0xc: cmove r11, qword ptr [r10] + 0x10: mov qword ptr [rbp - 8], r11 + "); + assert_snapshot!(cb.hexdump(), @"4c8b55f84c8b5df04d8b5b024d0f441a4c895df8"); + } } From 8090988f878c71c2aaefbb3123ac13e3753c93da Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 19 Nov 2025 20:53:41 -0500 Subject: [PATCH 09/14] ZJIT: Inline ArrayLength into LIR --- zjit/src/codegen.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index d00ab500d72796..06f991c738e869 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1426,7 +1426,14 @@ fn gen_array_pop(asm: &mut Assembler, array: Opnd, state: &FrameState) -> lir::O } fn gen_array_length(asm: &mut Assembler, array: Opnd) -> lir::Opnd { - asm_ccall!(asm, rb_jit_array_len, array) + let array = asm.load(array); + let flags = Opnd::mem(VALUE_BITS, array, RUBY_OFFSET_RBASIC_FLAGS); + let embedded_len = asm.and(flags, (RARRAY_EMBED_LEN_MASK as u64).into()); + let embedded_len = asm.rshift(embedded_len, (RARRAY_EMBED_LEN_SHIFT as u64).into()); + // cmov between the embedded length and heap length depending on the embed flag + asm.test(flags, (RARRAY_EMBED_FLAG as u64).into()); + let heap_len = Opnd::mem(c_long::BITS as u8, array, RUBY_OFFSET_RARRAY_AS_HEAP_LEN); + asm.csel_nz(embedded_len, heap_len) } /// Compile opt_newarray_hash - create a hash from array elements From 8728406c418f1a200cda02a259ba164d185a8ebd Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 21 Nov 2025 08:49:57 -0800 Subject: [PATCH 10/14] ZJIT: Inline Thread.current (#15272) Add `LoadEC` then it's just two `LoadField`. --- zjit/src/codegen.rs | 5 +++++ zjit/src/cruby.rs | 2 ++ zjit/src/cruby_methods.rs | 24 +++++++++++++++++++++++- zjit/src/hir.rs | 8 ++++++++ zjit/src/hir/opt_tests.rs | 6 ++++-- 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 06f991c738e869..b95d1372221bbd 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -457,6 +457,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::ArrayExtend { left, right, state } => { no_output!(gen_array_extend(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state))) }, &Insn::GuardShape { val, shape, state } => gen_guard_shape(jit, asm, opnd!(val), shape, &function.frame_state(state)), Insn::LoadPC => gen_load_pc(asm), + Insn::LoadEC => gen_load_ec(), Insn::LoadSelf => gen_load_self(), &Insn::LoadField { recv, id, offset, return_type: _ } => gen_load_field(asm, opnd!(recv), id, offset), &Insn::StoreField { recv, id, offset, val } => no_output!(gen_store_field(asm, opnd!(recv), id, offset, opnd!(val))), @@ -1041,6 +1042,10 @@ fn gen_load_pc(asm: &mut Assembler) -> Opnd { asm.load(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_PC)) } +fn gen_load_ec() -> Opnd { + EC +} + fn gen_load_self() -> Opnd { Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SELF) } diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 443ed0d86e3a99..83919e5369e5d9 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -1377,6 +1377,8 @@ pub(crate) mod ids { name: aref content: b"[]" name: len name: _as_heap + name: thread_ptr + name: self_ content: b"self" } /// Get an CRuby `ID` to an interned string, e.g. a particular method name. diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 190ac52eaceccb..d1f76e8da05dde 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -158,6 +158,7 @@ pub fn init() -> Annotations { ($module:ident, $method_name:literal, $inline:ident) => { let mut props = FnProperties::default(); props.inline = $inline; + #[allow(unused_unsafe)] annotate_c_method(cfuncs, unsafe { $module }, $method_name, props); }; ($module:ident, $method_name:literal, $inline:ident, $return_type:expr $(, $properties:ident)*) => { @@ -167,6 +168,7 @@ pub fn init() -> Annotations { $( props.$properties = true; )* + #[allow(unused_unsafe)] annotate_c_method(cfuncs, unsafe { $module }, $method_name, props); }; ($module:ident, $method_name:literal, $return_type:expr $(, $properties:ident)*) => { @@ -240,7 +242,7 @@ pub fn init() -> Annotations { annotate!(rb_cInteger, "<=", inline_integer_le); annotate!(rb_cString, "to_s", inline_string_to_s, types::StringExact); let thread_singleton = unsafe { rb_singleton_class(rb_cThread) }; - annotate!(thread_singleton, "current", types::BasicObject, no_gc, leaf); + annotate!(thread_singleton, "current", inline_thread_current, types::BasicObject, no_gc, leaf); annotate_builtin!(rb_mKernel, "Float", types::Float); annotate_builtin!(rb_mKernel, "Integer", types::Integer); @@ -269,6 +271,26 @@ fn inline_string_to_s(fun: &mut hir::Function, block: hir::BlockId, recv: hir::I None } +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: ID!(thread_ptr), + offset: RUBY_OFFSET_EC_THREAD_PTR as i32, + return_type: types::CPtr, + }); + let thread_self = fun.push_insn(block, hir::Insn::LoadField { + recv: thread_ptr, + id: ID!(self_), + offset: RUBY_OFFSET_THREAD_SELF as i32, + // TODO(max): Add Thread type. But Thread.current is not guaranteed to be an exact Thread. + // You can make subclasses... + return_type: types::BasicObject, + }); + Some(thread_self) +} + fn inline_kernel_itself(_fun: &mut hir::Function, _block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { if args.is_empty() { // No need to coerce the receiver; that is done by the SendWithoutBlock rewriting. diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 40c6092e56c471..333d5e5bff1837 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -736,6 +736,8 @@ pub enum Insn { /// Load cfp->pc LoadPC, + /// Load EC + LoadEC, /// Load cfp->self LoadSelf, LoadField { recv: InsnId, id: ID, offset: i32, return_type: Type }, @@ -980,6 +982,7 @@ impl Insn { Insn::GetLocal { .. } => false, Insn::IsNil { .. } => false, Insn::LoadPC => false, + Insn::LoadEC => false, Insn::LoadSelf => false, Insn::LoadField { .. } => false, Insn::CCall { elidable, .. } => !elidable, @@ -1272,6 +1275,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::DefinedIvar { self_val, id, .. } => write!(f, "DefinedIvar {self_val}, :{}", id.contents_lossy()), Insn::GetIvar { self_val, id, .. } => write!(f, "GetIvar {self_val}, :{}", id.contents_lossy()), Insn::LoadPC => write!(f, "LoadPC"), + Insn::LoadEC => write!(f, "LoadEC"), Insn::LoadSelf => write!(f, "LoadSelf"), &Insn::LoadField { recv, id, offset, return_type: _ } => write!(f, "LoadField {recv}, :{}@{:p}", id.contents_lossy(), self.ptr_map.map_offset(offset)), &Insn::StoreField { recv, id, offset, val } => write!(f, "StoreField {recv}, :{}@{:p}, {val}", id.contents_lossy(), self.ptr_map.map_offset(offset)), @@ -1775,6 +1779,7 @@ impl Function { | SideExit {..} | EntryPoint {..} | LoadPC + | LoadEC | LoadSelf | IncrCounterPtr {..} | IncrCounter(_)) => result.clone(), @@ -2069,6 +2074,7 @@ impl Function { Insn::GetGlobal { .. } => types::BasicObject, Insn::GetIvar { .. } => types::BasicObject, Insn::LoadPC => types::CPtr, + Insn::LoadEC => types::CPtr, Insn::LoadSelf => types::BasicObject, &Insn::LoadField { return_type, .. } => return_type, Insn::GetSpecialSymbol { .. } => types::BasicObject, @@ -3397,6 +3403,7 @@ impl Function { | &Insn::Param | &Insn::EntryPoint { .. } | &Insn::LoadPC + | &Insn::LoadEC | &Insn::LoadSelf | &Insn::GetLocal { .. } | &Insn::PutSpecialObject { .. } @@ -4101,6 +4108,7 @@ impl Function { | Insn::IsBlockGiven | Insn::GetGlobal { .. } | Insn::LoadPC + | Insn::LoadEC | Insn::LoadSelf | Insn::Snapshot { .. } | Insn::Jump { .. } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index b1ab8a06050d82..82f54f611a884c 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -5965,10 +5965,12 @@ mod hir_opt_tests { v20:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint MethodRedefined(Class@0x1010, current@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Class@0x1010) + v24:CPtr = LoadEC + v25:CPtr = LoadField v24, :thread_ptr@0x1048 + v26:BasicObject = LoadField v25, :self@0x1049 IncrCounter inline_cfunc_optimized_send_count - v25:BasicObject = CCall Thread.current@0x1048, v20 CheckInterrupts - Return v25 + Return v26 "); } From e0bb3fb1cda2238d0c98afcdec2fe282c29994aa Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 21 Nov 2025 08:57:26 -0800 Subject: [PATCH 11/14] ZJIT: Inline Integer#<< for constant rhs (#15258) This is good for protoboeuf and other binary parsing --- zjit/src/asm/arm64/mod.rs | 2 +- zjit/src/codegen.rs | 20 +++++++ zjit/src/cruby_methods.rs | 10 ++++ zjit/src/hir.rs | 10 +++- zjit/src/hir/opt_tests.rs | 109 ++++++++++++++++++++++++++++++++++++++ zjit/src/stats.rs | 2 + 6 files changed, 151 insertions(+), 2 deletions(-) diff --git a/zjit/src/asm/arm64/mod.rs b/zjit/src/asm/arm64/mod.rs index a4459117312f89..4094d101fbc32c 100644 --- a/zjit/src/asm/arm64/mod.rs +++ b/zjit/src/asm/arm64/mod.rs @@ -649,7 +649,7 @@ pub fn lsl(cb: &mut CodeBlock, rd: A64Opnd, rn: A64Opnd, shift: A64Opnd) { ShiftImm::lsl(rd.reg_no, rn.reg_no, uimm as u8, rd.num_bits).into() }, - _ => panic!("Invalid operands combination to lsl instruction") + _ => panic!("Invalid operands combination {rd:?} {rn:?} {shift:?} to lsl instruction") }; cb.write_bytes(&bytes); diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index b95d1372221bbd..9f74838c11fe87 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -402,6 +402,12 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::FixnumAnd { left, right } => gen_fixnum_and(asm, opnd!(left), opnd!(right)), Insn::FixnumOr { left, right } => gen_fixnum_or(asm, opnd!(left), opnd!(right)), Insn::FixnumXor { left, right } => gen_fixnum_xor(asm, opnd!(left), opnd!(right)), + &Insn::FixnumLShift { left, right, state } => { + // We only create FixnumLShift when we know the shift amount statically and it's in [0, + // 63]. + let shift_amount = function.type_of(right).fixnum_value().unwrap() as u64; + gen_fixnum_lshift(jit, asm, opnd!(left), shift_amount, &function.frame_state(state)) + } &Insn::FixnumMod { left, right, state } => gen_fixnum_mod(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), Insn::IsNil { val } => gen_isnil(asm, opnd!(val)), &Insn::IsMethodCfunc { val, cd, cfunc, state: _ } => gen_is_method_cfunc(jit, asm, opnd!(val), cd, cfunc), @@ -1700,6 +1706,20 @@ fn gen_fixnum_xor(asm: &mut Assembler, left: lir::Opnd, right: lir::Opnd) -> lir asm.add(out_val, Opnd::UImm(1)) } +/// Compile Fixnum << Fixnum +fn gen_fixnum_lshift(jit: &mut JITState, asm: &mut Assembler, left: lir::Opnd, shift_amount: u64, state: &FrameState) -> lir::Opnd { + // Shift amount is known statically to be in the range [0, 63] + assert!(shift_amount < 64); + let in_val = asm.sub(left, Opnd::UImm(1)); // Drop tag bit + let out_val = asm.lshift(in_val, shift_amount.into()); + let unshifted = asm.rshift(out_val, shift_amount.into()); + asm.cmp(in_val, unshifted); + asm.jne(side_exit(jit, state, FixnumLShiftOverflow)); + // Re-tag the output value + let out_val = asm.add(out_val, 1.into()); + out_val +} + fn gen_fixnum_mod(jit: &mut JITState, asm: &mut Assembler, left: lir::Opnd, right: lir::Opnd, state: &FrameState) -> lir::Opnd { // Check for left % 0, which raises ZeroDivisionError asm.cmp(right, Opnd::from(VALUE::fixnum_from_usize(0))); diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index d1f76e8da05dde..3999ef0a10f1d1 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -240,6 +240,7 @@ pub fn init() -> Annotations { annotate!(rb_cInteger, ">=", inline_integer_ge); annotate!(rb_cInteger, "<", inline_integer_lt); annotate!(rb_cInteger, "<=", inline_integer_le); + annotate!(rb_cInteger, "<<", inline_integer_lshift); annotate!(rb_cString, "to_s", inline_string_to_s, types::StringExact); let thread_singleton = unsafe { rb_singleton_class(rb_cThread) }; annotate!(thread_singleton, "current", inline_thread_current, types::BasicObject, no_gc, leaf); @@ -546,6 +547,15 @@ fn inline_integer_le(fun: &mut hir::Function, block: hir::BlockId, recv: hir::In try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumLe { left, right }, BOP_LE, recv, other, state) } +fn inline_integer_lshift(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { + let &[other] = args else { return None; }; + // Only convert to FixnumLShift if we know the shift amount is known at compile-time and could + // plausibly create a fixnum. + let Some(other_value) = fun.type_of(other).fixnum_value() else { return None; }; + if other_value < 0 || other_value > 63 { return None; } + try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumLShift { left, right, state }, BOP_LTLT, recv, other, state) +} + fn inline_basic_object_eq(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { let &[other] = args else { return None; }; let c_result = fun.push_insn(block, hir::Insn::IsBitEqual { left: recv, right: other }); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 333d5e5bff1837..00014c575897e3 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -485,6 +485,7 @@ pub enum SideExitReason { FixnumAddOverflow, FixnumSubOverflow, FixnumMultOverflow, + FixnumLShiftOverflow, GuardType(Type), GuardTypeNot(Type), GuardShape(ShapeId), @@ -868,7 +869,7 @@ pub enum Insn { /// Non-local control flow. See the throw YARV instruction Throw { throw_state: u32, val: InsnId, state: InsnId }, - /// Fixnum +, -, *, /, %, ==, !=, <, <=, >, >=, &, |, ^ + /// Fixnum +, -, *, /, %, ==, !=, <, <=, >, >=, &, |, ^, << FixnumAdd { left: InsnId, right: InsnId, state: InsnId }, FixnumSub { left: InsnId, right: InsnId, state: InsnId }, FixnumMult { left: InsnId, right: InsnId, state: InsnId }, @@ -883,6 +884,7 @@ pub enum Insn { FixnumAnd { left: InsnId, right: InsnId }, FixnumOr { left: InsnId, right: InsnId }, FixnumXor { left: InsnId, right: InsnId }, + FixnumLShift { left: InsnId, right: InsnId, state: InsnId }, // Distinct from `SendWithoutBlock` with `mid:to_s` because does not have a patch point for String to_s being redefined ObjToString { val: InsnId, cd: *const rb_call_data, state: InsnId }, @@ -979,6 +981,7 @@ impl Insn { Insn::FixnumAnd { .. } => false, Insn::FixnumOr { .. } => false, Insn::FixnumXor { .. } => false, + Insn::FixnumLShift { .. } => false, Insn::GetLocal { .. } => false, Insn::IsNil { .. } => false, Insn::LoadPC => false, @@ -1218,6 +1221,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::FixnumAnd { left, right, .. } => { write!(f, "FixnumAnd {left}, {right}") }, Insn::FixnumOr { left, right, .. } => { write!(f, "FixnumOr {left}, {right}") }, Insn::FixnumXor { left, right, .. } => { write!(f, "FixnumXor {left}, {right}") }, + Insn::FixnumLShift { left, right, .. } => { write!(f, "FixnumLShift {left}, {right}") }, Insn::GuardType { val, guard_type, .. } => { write!(f, "GuardType {val}, {}", guard_type.print(self.ptr_map)) }, Insn::GuardTypeNot { val, guard_type, .. } => { write!(f, "GuardTypeNot {val}, {}", guard_type.print(self.ptr_map)) }, Insn::GuardBitEquals { val, expected, .. } => { write!(f, "GuardBitEquals {val}, {}", expected.print(self.ptr_map)) }, @@ -1836,6 +1840,7 @@ impl Function { &FixnumAnd { left, right } => FixnumAnd { left: find!(left), right: find!(right) }, &FixnumOr { left, right } => FixnumOr { left: find!(left), right: find!(right) }, &FixnumXor { left, right } => FixnumXor { left: find!(left), right: find!(right) }, + &FixnumLShift { left, right, state } => FixnumLShift { left: find!(left), right: find!(right), state }, &ObjToString { val, cd, state } => ObjToString { val: find!(val), cd, @@ -2054,6 +2059,7 @@ impl Function { Insn::FixnumAnd { .. } => types::Fixnum, Insn::FixnumOr { .. } => types::Fixnum, Insn::FixnumXor { .. } => types::Fixnum, + Insn::FixnumLShift { .. } => types::Fixnum, Insn::PutSpecialObject { .. } => types::BasicObject, Insn::SendWithoutBlock { .. } => types::BasicObject, Insn::SendWithoutBlockDirect { .. } => types::BasicObject, @@ -3506,6 +3512,7 @@ impl Function { | &Insn::FixnumDiv { left, right, state } | &Insn::FixnumMod { left, right, state } | &Insn::ArrayExtend { left, right, state } + | &Insn::FixnumLShift { left, right, state } => { worklist.push_back(left); worklist.push_back(right); @@ -4271,6 +4278,7 @@ impl Function { | Insn::FixnumAnd { left, right } | Insn::FixnumOr { left, right } | Insn::FixnumXor { left, right } + | Insn::FixnumLShift { left, right, .. } | Insn::NewRangeFixnum { low: left, high: right, .. } => { self.assert_subtype(insn_id, left, types::Fixnum)?; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 82f54f611a884c..19c0ce66e36235 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6466,6 +6466,115 @@ mod hir_opt_tests { "); } + #[test] + fn test_inline_integer_ltlt_with_known_fixnum() { + eval(" + def test(x) = x << 5 + test(4) + "); + assert_contains_opcode("test", YARVINSN_opt_ltlt); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v14:Fixnum[5] = Const Value(5) + PatchPoint MethodRedefined(Integer@0x1000, <<@0x1008, cme:0x1010) + v24:Fixnum = GuardType v9, Fixnum + v25:Fixnum = FixnumLShift v24, v14 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v25 + "); + } + + #[test] + fn test_dont_inline_integer_ltlt_with_negative() { + eval(" + def test(x) = x << -5 + test(4) + "); + assert_contains_opcode("test", YARVINSN_opt_ltlt); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v14:Fixnum[-5] = Const Value(-5) + PatchPoint MethodRedefined(Integer@0x1000, <<@0x1008, cme:0x1010) + v24:Fixnum = GuardType v9, Fixnum + v25:BasicObject = CCallWithFrame Integer#<<@0x1038, v24, v14 + CheckInterrupts + Return v25 + "); + } + + #[test] + fn test_dont_inline_integer_ltlt_with_out_of_range() { + eval(" + def test(x) = x << 64 + test(4) + "); + assert_contains_opcode("test", YARVINSN_opt_ltlt); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + v14:Fixnum[64] = Const Value(64) + PatchPoint MethodRedefined(Integer@0x1000, <<@0x1008, cme:0x1010) + v24:Fixnum = GuardType v9, Fixnum + v25:BasicObject = CCallWithFrame Integer#<<@0x1038, v24, v14 + CheckInterrupts + Return v25 + "); + } + + #[test] + fn test_dont_inline_integer_ltlt_with_unknown_fixnum() { + eval(" + def test(x, y) = x << y + test(4, 5) + "); + assert_contains_opcode("test", YARVINSN_opt_ltlt); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@5 + v3:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2, v3) + bb1(v6:BasicObject, v7:BasicObject, v8:BasicObject): + EntryPoint JIT(0) + Jump bb2(v6, v7, v8) + bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): + PatchPoint MethodRedefined(Integer@0x1000, <<@0x1008, cme:0x1010) + v26:Fixnum = GuardType v11, Fixnum + v27:BasicObject = CCallWithFrame Integer#<<@0x1038, v26, v12 + CheckInterrupts + Return v27 + "); + } + #[test] fn test_optimize_string_append() { eval(r#" diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index df172997ce9793..1277db5b7e9e7d 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -142,6 +142,7 @@ make_counters! { exit_fixnum_add_overflow, exit_fixnum_sub_overflow, exit_fixnum_mult_overflow, + exit_fixnum_lshift_overflow, exit_fixnum_mod_by_zero, exit_box_fixnum_overflow, exit_guard_type_failure, @@ -423,6 +424,7 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { FixnumAddOverflow => exit_fixnum_add_overflow, FixnumSubOverflow => exit_fixnum_sub_overflow, FixnumMultOverflow => exit_fixnum_mult_overflow, + FixnumLShiftOverflow => exit_fixnum_lshift_overflow, FixnumModByZero => exit_fixnum_mod_by_zero, BoxFixnumOverflow => exit_box_fixnum_overflow, GuardType(_) => exit_guard_type_failure, From ff89e470e21e9d021c6739d83eddda4bd8c071fe Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 19 Nov 2025 16:37:45 -0500 Subject: [PATCH 12/14] ZJIT: Specialize Module#=== and Kernel#is_a? into IsA --- zjit/src/codegen.rs | 5 ++ zjit/src/cruby_methods.rs | 21 ++++- zjit/src/hir.rs | 15 ++++ zjit/src/hir/opt_tests.rs | 185 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 1 deletion(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 9f74838c11fe87..4c865dcd8a1b34 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -472,6 +472,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::ArrayInclude { elements, target, state } => gen_array_include(jit, asm, opnds!(elements), opnd!(target), &function.frame_state(*state)), &Insn::DupArrayInclude { ary, target, state } => gen_dup_array_include(jit, asm, ary, opnd!(target), &function.frame_state(state)), Insn::ArrayHash { elements, state } => gen_opt_newarray_hash(jit, asm, opnds!(elements), &function.frame_state(*state)), + &Insn::IsA { val, class } => gen_is_a(asm, opnd!(val), opnd!(class)), &Insn::ArrayMax { state, .. } | &Insn::FixnumDiv { state, .. } | &Insn::Throw { state, .. } @@ -1520,6 +1521,10 @@ fn gen_dup_array_include( ) } +fn gen_is_a(asm: &mut Assembler, obj: Opnd, class: Opnd) -> lir::Opnd { + asm_ccall!(asm, rb_obj_is_kind_of, obj, class) +} + /// Compile a new hash instruction fn gen_new_hash( jit: &mut JITState, diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 3999ef0a10f1d1..6d09b5e5a7995f 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -197,6 +197,7 @@ pub fn init() -> Annotations { annotate!(rb_mKernel, "itself", inline_kernel_itself); annotate!(rb_mKernel, "block_given?", inline_kernel_block_given_p); annotate!(rb_mKernel, "===", inline_eqq); + annotate!(rb_mKernel, "is_a?", inline_kernel_is_a_p); annotate!(rb_cString, "bytesize", inline_string_bytesize); annotate!(rb_cString, "size", types::Fixnum, no_gc, leaf, elidable); annotate!(rb_cString, "length", types::Fixnum, no_gc, leaf, elidable); @@ -206,7 +207,7 @@ pub fn init() -> Annotations { annotate!(rb_cString, "<<", inline_string_append); annotate!(rb_cString, "==", inline_string_eq); annotate!(rb_cModule, "name", types::StringExact.union(types::NilClass), no_gc, leaf, elidable); - annotate!(rb_cModule, "===", types::BoolExact, no_gc, leaf); + annotate!(rb_cModule, "===", inline_module_eqq, types::BoolExact, no_gc, leaf); annotate!(rb_cArray, "length", types::Fixnum, no_gc, leaf, elidable); annotate!(rb_cArray, "size", types::Fixnum, no_gc, leaf, elidable); annotate!(rb_cArray, "empty?", types::BoolExact, no_gc, leaf, elidable); @@ -447,6 +448,15 @@ fn inline_string_eq(fun: &mut hir::Function, block: hir::BlockId, recv: hir::Ins None } +fn inline_module_eqq(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { + let &[other] = args else { return None; }; + if fun.is_a(recv, types::Class) { + let result = fun.push_insn(block, hir::Insn::IsA { val: other, class: recv }); + return Some(result); + } + None +} + fn inline_integer_succ(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { if !args.is_empty() { return None; } if fun.likely_a(recv, types::Fixnum, state) { @@ -613,6 +623,15 @@ fn inline_eqq(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, a Some(result) } +fn inline_kernel_is_a_p(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { + let &[other] = args else { return None; }; + if fun.is_a(other, types::Class) { + let result = fun.push_insn(block, hir::Insn::IsA { val: recv, class: other }); + return Some(result); + } + None +} + fn inline_kernel_nil_p(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { if !args.is_empty() { return None; } Some(fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(Qfalse) })) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 00014c575897e3..3a69dd6610e514 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -721,6 +721,9 @@ pub enum Insn { /// Test the bit at index of val, a Fixnum. /// Return Qtrue if the bit is set, else Qfalse. FixnumBitCheck { val: InsnId, index: u8 }, + /// Return Qtrue if `val` is an instance of `class`, else Qfalse. + /// Equivalent to `class_search_ancestor(CLASS_OF(val), class)`. + IsA { val: InsnId, class: InsnId }, /// Get a global variable named `id` GetGlobal { id: ID, state: InsnId }, @@ -1000,6 +1003,7 @@ impl Insn { Insn::BoxFixnum { .. } => false, Insn::BoxBool { .. } => false, Insn::IsBitEqual { .. } => false, + Insn::IsA { .. } => false, _ => true, } } @@ -1324,6 +1328,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { } Insn::IncrCounter(counter) => write!(f, "IncrCounter {counter:?}"), Insn::CheckInterrupts { .. } => write!(f, "CheckInterrupts"), + Insn::IsA { val, class } => write!(f, "IsA {val}, {class}"), } } } @@ -1946,6 +1951,7 @@ impl Function { &ArrayExtend { left, right, state } => ArrayExtend { left: find!(left), right: find!(right), state }, &ArrayPush { array, val, state } => ArrayPush { array: find!(array), val: find!(val), state }, &CheckInterrupts { state } => CheckInterrupts { state }, + &IsA { val, class } => IsA { val: find!(val), class: find!(class) }, } } @@ -2095,6 +2101,7 @@ impl Function { // The type of Snapshot doesn't really matter; it's never materialized. It's used only // as a reference for FrameState, which we use to generate side-exit code. Insn::Snapshot { .. } => types::Any, + Insn::IsA { .. } => types::BoolExact, } } @@ -3622,6 +3629,10 @@ impl Function { &Insn::ObjectAllocClass { state, .. } | &Insn::SideExit { state, .. } => worklist.push_back(state), &Insn::UnboxFixnum { val } => worklist.push_back(val), + &Insn::IsA { val, class } => { + worklist.push_back(val); + worklist.push_back(class); + } } } @@ -4314,6 +4325,10 @@ impl Function { self.assert_subtype(insn_id, index, types::Fixnum)?; self.assert_subtype(insn_id, value, types::Fixnum) } + Insn::IsA { val, class } => { + self.assert_subtype(insn_id, val, types::BasicObject)?; + self.assert_subtype(insn_id, class, types::Class) + } } } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 19c0ce66e36235..9704afcf6e770c 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -7983,6 +7983,191 @@ mod hir_opt_tests { "); } + #[test] + fn test_specialize_class_eqq() { + eval(r#" + def test(o) = String === o + test("asdf") + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, String) + v26:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint NoEPEscape(test) + PatchPoint MethodRedefined(Class@0x1010, ===@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(Class@0x1010) + v30:BoolExact = IsA v9, v26 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v30 + "); + } + + #[test] + fn test_dont_specialize_module_eqq() { + eval(r#" + def test(o) = Kernel === o + test("asdf") + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, Kernel) + v26:ModuleExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint NoEPEscape(test) + PatchPoint MethodRedefined(Module@0x1010, ===@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(Module@0x1010) + IncrCounter inline_cfunc_optimized_send_count + v31:BoolExact = CCall Module#===@0x1048, v26, v9 + CheckInterrupts + Return v31 + "); + } + + #[test] + fn test_specialize_is_a_class() { + eval(r#" + def test(o) = o.is_a?(String) + test("asdf") + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, String) + v24:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(String@0x1008, is_a?@0x1010, cme:0x1018) + PatchPoint NoSingletonClass(String@0x1008) + v28:StringExact = GuardType v9, StringExact + v29:BoolExact = IsA v28, v24 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v29 + "); + } + + #[test] + fn test_dont_specialize_is_a_module() { + eval(r#" + def test(o) = o.is_a?(Kernel) + test("asdf") + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, Kernel) + v24:ModuleExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(String@0x1010, is_a?@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(String@0x1010) + v28:StringExact = GuardType v9, StringExact + v29:BasicObject = CCallWithFrame Kernel#is_a?@0x1048, v28, v24 + CheckInterrupts + Return v29 + "); + } + + #[test] + fn test_elide_is_a() { + eval(r#" + def test(o) + o.is_a?(Integer) + 5 + end + test("asdf") + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, Integer) + v28:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(String@0x1010, is_a?@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(String@0x1010) + v32:StringExact = GuardType v9, StringExact + IncrCounter inline_cfunc_optimized_send_count + v21:Fixnum[5] = Const Value(5) + CheckInterrupts + Return v21 + "); + } + + #[test] + fn test_elide_class_eqq() { + eval(r#" + def test(o) + Integer === o + 5 + end + test("asdf") + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, Integer) + v30:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint NoEPEscape(test) + PatchPoint MethodRedefined(Class@0x1010, ===@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(Class@0x1010) + IncrCounter inline_cfunc_optimized_send_count + v23:Fixnum[5] = Const Value(5) + CheckInterrupts + Return v23 + "); + } + #[test] fn counting_complex_feature_use_for_fallback() { eval(" From 14e34fa7c0f02402b322bc9bbdd34fec446b70e9 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 20 Nov 2025 17:56:17 -0500 Subject: [PATCH 13/14] ZJIT: Print class objects more nicely in HIR --- zjit/src/hir/opt_tests.rs | 86 +++++++++++++++++++-------------------- zjit/src/hir_type/mod.rs | 2 + 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 9704afcf6e770c..70ba333b1be5d5 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -2299,7 +2299,7 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, C) - v18:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v18:Class[C@0x1008] = Const Value(VALUE(0x1008)) CheckInterrupts Return v18 "); @@ -2323,16 +2323,16 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, String) - v29:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v29:Class[String@0x1008] = Const Value(VALUE(0x1008)) PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1010, Class) - v32:Class[VALUE(0x1018)] = Const Value(VALUE(0x1018)) + v32:Class[Class@0x1018] = Const Value(VALUE(0x1018)) PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1020, Module) - v35:Class[VALUE(0x1028)] = Const Value(VALUE(0x1028)) + v35:Class[Module@0x1028] = Const Value(VALUE(0x1028)) PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1030, BasicObject) - v38:Class[VALUE(0x1038)] = Const Value(VALUE(0x1038)) + v38:Class[BasicObject@0x1038] = Const Value(VALUE(0x1038)) v22:ArrayExact = NewArray v29, v32, v35, v38 CheckInterrupts Return v22 @@ -2954,7 +2954,7 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Foo::Bar::C) - v18:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v18:Class[Foo::Bar::C@0x1008] = Const Value(VALUE(0x1008)) CheckInterrupts Return v18 "); @@ -2979,11 +2979,11 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, C) - v43:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v43:Class[C@0x1008] = Const Value(VALUE(0x1008)) v13:NilClass = Const Value(nil) - PatchPoint MethodRedefined(C@0x1008, new@0x1010, cme:0x1018) + PatchPoint MethodRedefined(C@0x1008, new@0x1009, cme:0x1010) v46:HeapObject[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) - PatchPoint MethodRedefined(C@0x1008, initialize@0x1040, cme:0x1048) + PatchPoint MethodRedefined(C@0x1008, initialize@0x1038, cme:0x1040) PatchPoint NoSingletonClass(C@0x1008) v50:NilClass = Const Value(nil) IncrCounter inline_cfunc_optimized_send_count @@ -3016,14 +3016,14 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, C) - v46:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v46:Class[C@0x1008] = Const Value(VALUE(0x1008)) v13:NilClass = Const Value(nil) v16:Fixnum[1] = Const Value(1) - PatchPoint MethodRedefined(C@0x1008, new@0x1010, cme:0x1018) + PatchPoint MethodRedefined(C@0x1008, new@0x1009, cme:0x1010) v49:HeapObject[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) - PatchPoint MethodRedefined(C@0x1008, initialize@0x1040, cme:0x1048) + PatchPoint MethodRedefined(C@0x1008, initialize@0x1038, cme:0x1040) PatchPoint NoSingletonClass(C@0x1008) - v52:BasicObject = SendWithoutBlockDirect v49, :initialize (0x1070), v16 + v52:BasicObject = SendWithoutBlockDirect v49, :initialize (0x1068), v16 CheckInterrupts CheckInterrupts Return v49 @@ -3048,11 +3048,11 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Object) - v43:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v43:Class[Object@0x1008] = Const Value(VALUE(0x1008)) v13:NilClass = Const Value(nil) - PatchPoint MethodRedefined(Object@0x1008, new@0x1010, cme:0x1018) + PatchPoint MethodRedefined(Object@0x1008, new@0x1009, cme:0x1010) v46:ObjectExact = ObjectAllocClass Object:VALUE(0x1008) - PatchPoint MethodRedefined(Object@0x1008, initialize@0x1040, cme:0x1048) + PatchPoint MethodRedefined(Object@0x1008, initialize@0x1038, cme:0x1040) PatchPoint NoSingletonClass(Object@0x1008) v50:NilClass = Const Value(nil) IncrCounter inline_cfunc_optimized_send_count @@ -3080,11 +3080,11 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, BasicObject) - v43:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v43:Class[BasicObject@0x1008] = Const Value(VALUE(0x1008)) v13:NilClass = Const Value(nil) - PatchPoint MethodRedefined(BasicObject@0x1008, new@0x1010, cme:0x1018) + PatchPoint MethodRedefined(BasicObject@0x1008, new@0x1009, cme:0x1010) v46:BasicObjectExact = ObjectAllocClass BasicObject:VALUE(0x1008) - PatchPoint MethodRedefined(BasicObject@0x1008, initialize@0x1040, cme:0x1048) + PatchPoint MethodRedefined(BasicObject@0x1008, initialize@0x1038, cme:0x1040) PatchPoint NoSingletonClass(BasicObject@0x1008) v50:NilClass = Const Value(nil) IncrCounter inline_cfunc_optimized_send_count @@ -3112,9 +3112,9 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Hash) - v43:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v43:Class[Hash@0x1008] = Const Value(VALUE(0x1008)) v13:NilClass = Const Value(nil) - PatchPoint MethodRedefined(Hash@0x1008, new@0x1010, cme:0x1018) + PatchPoint MethodRedefined(Hash@0x1008, new@0x1009, cme:0x1010) v46:HashExact = ObjectAllocClass Hash:VALUE(0x1008) IncrCounter complex_arg_pass_param_kw IncrCounter complex_arg_pass_param_block @@ -3144,13 +3144,13 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Array) - v46:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v46:Class[Array@0x1008] = Const Value(VALUE(0x1008)) v13:NilClass = Const Value(nil) v16:Fixnum[1] = Const Value(1) - PatchPoint MethodRedefined(Array@0x1008, new@0x1010, cme:0x1018) - PatchPoint MethodRedefined(Class@0x1040, new@0x1010, cme:0x1018) - PatchPoint NoSingletonClass(Class@0x1040) - v57:BasicObject = CCallVariadic Array.new@0x1048, v46, v16 + PatchPoint MethodRedefined(Array@0x1008, new@0x1009, cme:0x1010) + PatchPoint MethodRedefined(Class@0x1038, new@0x1009, cme:0x1010) + PatchPoint NoSingletonClass(Class@0x1038) + v57:BasicObject = CCallVariadic Array.new@0x1040, v46, v16 CheckInterrupts Return v57 "); @@ -3174,14 +3174,14 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Set) - v43:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v43:Class[Set@0x1008] = Const Value(VALUE(0x1008)) v13:NilClass = Const Value(nil) - PatchPoint MethodRedefined(Set@0x1008, new@0x1010, cme:0x1018) + PatchPoint MethodRedefined(Set@0x1008, new@0x1009, cme:0x1010) v18:HeapBasicObject = ObjectAlloc v43 - PatchPoint MethodRedefined(Set@0x1008, initialize@0x1040, cme:0x1048) + PatchPoint MethodRedefined(Set@0x1008, initialize@0x1038, cme:0x1040) PatchPoint NoSingletonClass(Set@0x1008) v49:SetExact = GuardType v18, SetExact - v50:BasicObject = CCallVariadic Set#initialize@0x1070, v49 + v50:BasicObject = CCallVariadic Set#initialize@0x1068, v49 CheckInterrupts CheckInterrupts Return v18 @@ -3206,12 +3206,12 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, String) - v43:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v43:Class[String@0x1008] = Const Value(VALUE(0x1008)) v13:NilClass = Const Value(nil) - PatchPoint MethodRedefined(String@0x1008, new@0x1010, cme:0x1018) - PatchPoint MethodRedefined(Class@0x1040, new@0x1010, cme:0x1018) - PatchPoint NoSingletonClass(Class@0x1040) - v54:BasicObject = CCallVariadic String.new@0x1048, v43 + PatchPoint MethodRedefined(String@0x1008, new@0x1009, cme:0x1010) + PatchPoint MethodRedefined(Class@0x1038, new@0x1009, cme:0x1010) + PatchPoint NoSingletonClass(Class@0x1038) + v54:BasicObject = CCallVariadic String.new@0x1040, v43 CheckInterrupts Return v54 "); @@ -3235,7 +3235,7 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Regexp) - v47:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v47:Class[Regexp@0x1008] = Const Value(VALUE(0x1008)) v13:NilClass = Const Value(nil) v16:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010)) v17:StringExact = StringCopy v16 @@ -4406,7 +4406,7 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Foo) - v22:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v22:Class[Foo@0x1008] = Const Value(VALUE(0x1008)) v13:Fixnum[100] = Const Value(100) PatchPoint MethodRedefined(Class@0x1010, identity@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Class@0x1010) @@ -5962,7 +5962,7 @@ mod hir_opt_tests { bb2(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Thread) - v20:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v20:Class[Thread@0x1008] = Const Value(VALUE(0x1008)) PatchPoint MethodRedefined(Class@0x1010, current@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Class@0x1010) v24:CPtr = LoadEC @@ -8002,7 +8002,7 @@ mod hir_opt_tests { bb2(v8:BasicObject, v9:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, String) - v26:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v26:Class[String@0x1008] = Const Value(VALUE(0x1008)) PatchPoint NoEPEscape(test) PatchPoint MethodRedefined(Class@0x1010, ===@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Class@0x1010) @@ -8062,8 +8062,8 @@ mod hir_opt_tests { bb2(v8:BasicObject, v9:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, String) - v24:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - PatchPoint MethodRedefined(String@0x1008, is_a?@0x1010, cme:0x1018) + v24:Class[String@0x1008] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(String@0x1008, is_a?@0x1009, cme:0x1010) PatchPoint NoSingletonClass(String@0x1008) v28:StringExact = GuardType v9, StringExact v29:BoolExact = IsA v28, v24 @@ -8124,7 +8124,7 @@ mod hir_opt_tests { bb2(v8:BasicObject, v9:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Integer) - v28:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v28:Class[Integer@0x1008] = Const Value(VALUE(0x1008)) PatchPoint MethodRedefined(String@0x1010, is_a?@0x1018, cme:0x1020) PatchPoint NoSingletonClass(String@0x1010) v32:StringExact = GuardType v9, StringExact @@ -8157,7 +8157,7 @@ mod hir_opt_tests { bb2(v8:BasicObject, v9:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Integer) - v30:Class[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v30:Class[Integer@0x1008] = Const Value(VALUE(0x1008)) PatchPoint NoEPEscape(test) PatchPoint MethodRedefined(Class@0x1010, ===@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Class@0x1010) diff --git a/zjit/src/hir_type/mod.rs b/zjit/src/hir_type/mod.rs index 7c10ef4425c6fd..8e862d74e71aba 100644 --- a/zjit/src/hir_type/mod.rs +++ b/zjit/src/hir_type/mod.rs @@ -77,6 +77,8 @@ fn write_spec(f: &mut std::fmt::Formatter, printer: &TypePrinter) -> std::fmt::R Specialization::Object(val) if val == unsafe { rb_mRubyVMFrozenCore } => write!(f, "[VMFrozenCore]"), Specialization::Object(val) if val == unsafe { rb_block_param_proxy } => write!(f, "[BlockParamProxy]"), Specialization::Object(val) if ty.is_subtype(types::Symbol) => write!(f, "[:{}]", ruby_sym_to_rust_string(val)), + Specialization::Object(val) if ty.is_subtype(types::Class) => + write!(f, "[{}@{:p}]", get_class_name(val), printer.ptr_map.map_ptr(val.0 as *const std::ffi::c_void)), Specialization::Object(val) => write!(f, "[{}]", val.print(printer.ptr_map)), // TODO(max): Ensure singleton classes never have Type specialization Specialization::Type(val) if unsafe { rb_zjit_singleton_class_p(val) } => From 1959fcacb357ec548ed8a000c6dc6e5f39a3fb55 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 21 Nov 2025 11:53:35 -0500 Subject: [PATCH 14/14] ZJIT: Add tests for Kernel#kind_of? --- zjit/src/hir/opt_tests.rs | 92 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 70ba333b1be5d5..c99f01d088ef86 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -8168,6 +8168,98 @@ mod hir_opt_tests { "); } + #[test] + fn test_specialize_kind_of_class() { + eval(r#" + def test(o) = o.kind_of?(String) + test("asdf") + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, String) + v24:Class[String@0x1008] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(String@0x1008, kind_of?@0x1009, cme:0x1010) + PatchPoint NoSingletonClass(String@0x1008) + v28:StringExact = GuardType v9, StringExact + v29:BoolExact = IsA v28, v24 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v29 + "); + } + + #[test] + fn test_dont_specialize_kind_of_module() { + eval(r#" + def test(o) = o.kind_of?(Kernel) + test("asdf") + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, Kernel) + v24:ModuleExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(String@0x1010, kind_of?@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(String@0x1010) + v28:StringExact = GuardType v9, StringExact + v29:BasicObject = CCallWithFrame Kernel#kind_of?@0x1048, v28, v24 + CheckInterrupts + Return v29 + "); + } + + #[test] + fn test_elide_kind_of() { + eval(r#" + def test(o) + o.kind_of?(Integer) + 5 + end + test("asdf") + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal l0, SP@4 + Jump bb2(v1, v2) + bb1(v5:BasicObject, v6:BasicObject): + EntryPoint JIT(0) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:BasicObject): + PatchPoint SingleRactorMode + PatchPoint StableConstantNames(0x1000, Integer) + v28:Class[Integer@0x1008] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(String@0x1010, kind_of?@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(String@0x1010) + v32:StringExact = GuardType v9, StringExact + IncrCounter inline_cfunc_optimized_send_count + v21:Fixnum[5] = Const Value(5) + CheckInterrupts + Return v21 + "); + } + #[test] fn counting_complex_feature_use_for_fallback() { eval("