From b51ae934f7c81f4ef17d74cd0788e9eefac44149 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 20 Jul 2026 19:27:45 +0900 Subject: [PATCH 1/9] [Feature #22215] Remove vm_core dependency from gc header Keep `internal/gc.h` usable without VM-private definitions so bundled extensions do not inherit `vm_core.h` dependencies. --- bignum.c | 1 + ext/socket/raddrinfo.c | 1 + internal/box.h | 4 + internal/gc.h | 8 +- internal/thread.h | 1 + memory_view.c | 1 + node.c | 3 +- numeric.c | 1 + parse.y | 4 +- rational.c | 1 + shape.h | 1 + string.c | 1 + symbol.c | 1 + yjit/src/cruby_bindings.inc.rs | 94 ++++++++++---------- zjit/src/cruby_bindings.inc.rs | 152 ++++++++++++++++----------------- 15 files changed, 146 insertions(+), 128 deletions(-) diff --git a/bignum.c b/bignum.c index b72b18ad08ab79..d95444a56378e3 100644 --- a/bignum.c +++ b/bignum.c @@ -45,6 +45,7 @@ #include "ruby/thread.h" #include "ruby/util.h" #include "ruby_assert.h" +#include "vm_core.h" /* for GET_EC() */ #if USE_GMP RBIMPL_WARNING_PUSH() diff --git a/ext/socket/raddrinfo.c b/ext/socket/raddrinfo.c index 6a565789952952..1d85835ba531c1 100644 --- a/ext/socket/raddrinfo.c +++ b/ext/socket/raddrinfo.c @@ -9,6 +9,7 @@ ************************************************/ #include "rubysocket.h" +#include // GETADDRINFO_IMPL == 0 : call getaddrinfo/getnameinfo directly // GETADDRINFO_IMPL == 1 : call getaddrinfo/getnameinfo without gvl (but uncancellable) diff --git a/internal/box.h b/internal/box.h index 1958b8dca6b84a..1e7d4fbdef5169 100644 --- a/internal/box.h +++ b/internal/box.h @@ -94,4 +94,8 @@ void rb_box_cleanup_local_extension(VALUE cleanup); void rb_initialize_mandatory_boxes(void); void rb_box_init_done(void); void rb_box_set_gem_flags(rb_box_gem_flags_t *); + +/* variable.c */ +void rb_autoload_copy_table_for_box(st_table *, const rb_box_t *); + #endif /* INTERNAL_BOX_H */ diff --git a/internal/gc.h b/internal/gc.h index 9a137d2609554f..706610f35eec4c 100644 --- a/internal/gc.h +++ b/internal/gc.h @@ -14,7 +14,7 @@ #include "internal/compilers.h" /* for __has_attribute */ #include "ruby/ruby.h" /* for rb_event_flag_t */ -#include "vm_core.h" /* for GET_EC() */ +#include "ruby_atomic.h" /* for RUBY_ATOMIC_VALUE_SET */ struct rb_gc_zjit_fastpath; @@ -121,6 +121,7 @@ const char *rb_raw_obj_info(char *const buff, const size_t buff_size, VALUE obj) struct rb_execution_context_struct; /* in vm_core.h */ struct rb_objspace; /* in vm_core.h */ +struct rb_ractor_struct; /* in vm_core.h */ #define EC_NEWOBJ_OF(var, T, c, f, s, ec) \ T *(var) = (T *)rb_ec_newobj_of((ec), (c), (f), s) @@ -200,9 +201,10 @@ void *rb_xrealloc_mul_add(const void *, size_t, size_t, size_t); RUBY_ATTR_MALLOC void *rb_xmalloc_mul_add_mul(size_t, size_t, size_t, size_t); RUBY_ATTR_MALLOC void *rb_xcalloc_mul_add_mul(size_t, size_t, size_t, size_t); void rb_gc_register_pinning_obj(VALUE obj); -rb_execution_context_t *rb_gc_get_ec(void); +struct rb_execution_context_struct *rb_gc_get_ec(void); +VALUE rb_newobj_of_unprotected(VALUE klass, VALUE flags, size_t size); -void *rb_gc_ractor_cache_alloc(rb_ractor_t *ractor); +void *rb_gc_ractor_cache_alloc(struct rb_ractor_struct *ractor); void rb_gc_ractor_cache_free(void *cache); bool rb_gc_zjit_new_obj_fastpath(size_t alloc_size, VALUE flags, VALUE klass, struct rb_gc_zjit_fastpath *fastpath); diff --git a/internal/thread.h b/internal/thread.h index ea891b4372f8f4..ad5b645c932dae 100644 --- a/internal/thread.h +++ b/internal/thread.h @@ -12,6 +12,7 @@ #include "ruby/intern.h" /* for rb_blocking_function_t */ #include "ccan/list/list.h" /* for list in rb_io_close_wait_list */ +struct rb_execution_context_struct; /* in vm_core.h */ struct rb_thread_struct; /* in vm_core.h */ struct rb_io; diff --git a/memory_view.c b/memory_view.c index f8d7508f68c24f..2c088e7dab511c 100644 --- a/memory_view.c +++ b/memory_view.c @@ -9,6 +9,7 @@ #include "internal.h" #include "internal/hash.h" #include "internal/variable.h" +#include "internal/vm.h" #include "ruby/memory_view.h" #include "ruby/util.h" #include "vm_sync.h" diff --git a/node.c b/node.c index 5d00823eb67fc7..2521e554e0f724 100644 --- a/node.c +++ b/node.c @@ -11,11 +11,12 @@ #ifdef UNIVERSAL_PARSER #include -#include "node.h" #include "rubyparser.h" #endif +#include "internal.h" #include "internal/variable.h" +#include "node.h" #define NODE_BUF_DEFAULT_SIZE (sizeof(struct RNode) * 16) diff --git a/numeric.c b/numeric.c index cb7b4932e963b9..0760312e0ea9d5 100644 --- a/numeric.c +++ b/numeric.c @@ -39,6 +39,7 @@ #include "internal/string.h" #include "internal/util.h" #include "internal/variable.h" +#include "vm_core.h" #include "ruby/encoding.h" #include "ruby/util.h" #include "builtin.h" diff --git a/parse.y b/parse.y index 2afeb62097471e..f55b6374fef5d8 100644 --- a/parse.y +++ b/parse.y @@ -24,6 +24,7 @@ #endif #include "ruby/internal/config.h" +#include "internal/thread.h" #include @@ -41,11 +42,13 @@ #else #include "internal.h" +#include "internal/array.h" #include "internal/compile.h" #include "internal/compilers.h" #include "internal/complex.h" #include "internal/encoding.h" #include "internal/error.h" +#include "internal/gc.h" #include "internal/hash.h" #include "internal/io.h" #include "internal/numeric.h" @@ -55,7 +58,6 @@ #include "internal/ruby_parser.h" #include "internal/symbol.h" #include "internal/thread.h" -#include "internal/variable.h" #include "node.h" #include "parser_node.h" #include "probes.h" diff --git a/rational.c b/rational.c index f40c412506e273..30c3e6daffcbf6 100644 --- a/rational.c +++ b/rational.c @@ -33,6 +33,7 @@ #include "internal/object.h" #include "internal/rational.h" #include "ruby_assert.h" +#include "vm_core.h" /* for GET_EC() */ #if USE_GMP RBIMPL_WARNING_PUSH() diff --git a/shape.h b/shape.h index 68fd4a7c9f332c..2a416b1b126e32 100644 --- a/shape.h +++ b/shape.h @@ -2,6 +2,7 @@ #define RUBY_SHAPE_H #include "internal/gc.h" +#include "internal/imemo.h" typedef uint8_t attr_index_t; typedef uint32_t shape_id_t; diff --git a/string.c b/string.c index 18c31812c71623..bc5434ef6ae1f9 100644 --- a/string.c +++ b/string.c @@ -49,6 +49,7 @@ #include "ruby/ractor.h" #include "ruby_assert.h" #include "shape.h" +#include "vm_core.h" #include "vm_sync.h" #include "zjit.h" #include "ruby/internal/attr/nonstring.h" diff --git a/symbol.c b/symbol.c index e8a1a177b0fd56..48e3569c8fbf0a 100644 --- a/symbol.c +++ b/symbol.c @@ -24,6 +24,7 @@ #include "ruby/ractor.h" #include "ruby/st.h" #include "symbol.h" +#include "vm_core.h" #include "vm_sync.h" #include "builtin.h" #include "ruby/internal/attr/nonstring.h" diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 659c0af223bfb7..753955c8affb0a 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -153,6 +153,7 @@ impl ::std::cmp::PartialEq for __BindgenUnionField { } } impl ::std::cmp::Eq for __BindgenUnionField {} +pub const SHAPE_ID_NUM_BITS: u32 = 32; pub const INTEGER_REDEFINED_OP_FLAG: u32 = 1; pub const FLOAT_REDEFINED_OP_FLAG: u32 = 2; pub const STRING_REDEFINED_OP_FLAG: u32 = 4; @@ -171,7 +172,6 @@ pub const VM_ENV_DATA_INDEX_ME_CREF: i32 = -2; pub const VM_ENV_DATA_INDEX_SPECVAL: i32 = -1; pub const VM_ENV_DATA_INDEX_FLAGS: u32 = 0; pub const VM_BLOCK_HANDLER_NONE: u32 = 0; -pub const SHAPE_ID_NUM_BITS: u32 = 32; pub type ID = ::std::os::raw::c_ulong; pub type rb_alloc_func_t = ::std::option::Option VALUE>; pub const RUBY_Qfalse: ruby_special_consts = 0; @@ -300,6 +300,42 @@ pub const RUBY_ENCINDEX_EUC_JP: ruby_preserved_encindex = 10; pub const RUBY_ENCINDEX_Windows_31J: ruby_preserved_encindex = 11; pub const RUBY_ENCINDEX_BUILTIN_MAX: ruby_preserved_encindex = 12; pub type ruby_preserved_encindex = u32; +pub type rb_atomic_t = ::std::os::raw::c_uint; +pub const imemo_env: imemo_type = 0; +pub const imemo_cref: imemo_type = 1; +pub const imemo_svar: imemo_type = 2; +pub const imemo_throw_data: imemo_type = 3; +pub const imemo_ifunc: imemo_type = 4; +pub const imemo_memo: imemo_type = 5; +pub const imemo_ment: imemo_type = 6; +pub const imemo_iseq: imemo_type = 7; +pub const imemo_tmpbuf: imemo_type = 8; +pub const imemo_cvar_entry: imemo_type = 9; +pub const imemo_callinfo: imemo_type = 10; +pub const imemo_callcache: imemo_type = 11; +pub const imemo_constcache: imemo_type = 12; +pub const imemo_fields: imemo_type = 13; +pub const imemo_subclasses: imemo_type = 14; +pub const imemo_cdhash: imemo_type = 15; +pub type imemo_type = u32; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct vm_ifunc_argc { + pub min: ::std::os::raw::c_int, + pub max: ::std::os::raw::c_int, +} +#[repr(C)] +pub struct vm_ifunc { + pub flags: VALUE, + pub svar_lep: *mut VALUE, + pub func: rb_block_call_func_t, + pub data: *const ::std::os::raw::c_void, + pub argc: vm_ifunc_argc, +} +pub type attr_index_t = u8; +pub type shape_id_t = u32; +pub const SHAPE_ID_HAS_IVAR_MASK: shape_id_mask = 67633150; +pub type shape_id_mask = u32; pub const BOP_PLUS: ruby_basic_operators = 0; pub const BOP_MINUS: ruby_basic_operators = 1; pub const BOP_MULT: ruby_basic_operators = 2; @@ -339,38 +375,6 @@ pub const BOP_YIELD: ruby_basic_operators = 35; pub const BOP_LAST_: ruby_basic_operators = 36; pub type ruby_basic_operators = u32; pub type rb_serial_t = ::std::os::raw::c_ulonglong; -pub const imemo_env: imemo_type = 0; -pub const imemo_cref: imemo_type = 1; -pub const imemo_svar: imemo_type = 2; -pub const imemo_throw_data: imemo_type = 3; -pub const imemo_ifunc: imemo_type = 4; -pub const imemo_memo: imemo_type = 5; -pub const imemo_ment: imemo_type = 6; -pub const imemo_iseq: imemo_type = 7; -pub const imemo_tmpbuf: imemo_type = 8; -pub const imemo_cvar_entry: imemo_type = 9; -pub const imemo_callinfo: imemo_type = 10; -pub const imemo_callcache: imemo_type = 11; -pub const imemo_constcache: imemo_type = 12; -pub const imemo_fields: imemo_type = 13; -pub const imemo_subclasses: imemo_type = 14; -pub const imemo_cdhash: imemo_type = 15; -pub type imemo_type = u32; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct vm_ifunc_argc { - pub min: ::std::os::raw::c_int, - pub max: ::std::os::raw::c_int, -} -#[repr(C)] -pub struct vm_ifunc { - pub flags: VALUE, - pub svar_lep: *mut VALUE, - pub func: rb_block_call_func_t, - pub data: *const ::std::os::raw::c_void, - pub argc: vm_ifunc_argc, -} -pub type rb_atomic_t = ::std::os::raw::c_uint; pub const METHOD_VISI_UNDEF: rb_method_visibility_t = 0; pub const METHOD_VISI_PUBLIC: rb_method_visibility_t = 1; pub const METHOD_VISI_PRIVATE: rb_method_visibility_t = 2; @@ -645,10 +649,6 @@ pub const VM_ENV_FLAG_ESCAPED: vm_frame_env_flags = 4; pub const VM_ENV_FLAG_WB_REQUIRED: vm_frame_env_flags = 8; pub const VM_ENV_FLAG_ISOLATED: vm_frame_env_flags = 16; pub type vm_frame_env_flags = u32; -pub type attr_index_t = u8; -pub type shape_id_t = u32; -pub const SHAPE_ID_HAS_IVAR_MASK: shape_id_mask = 67633150; -pub type shape_id_mask = u32; #[repr(C)] pub struct rb_cvar_class_tbl_entry { pub imemo_flags: VALUE, @@ -1098,6 +1098,8 @@ extern "C" { elements: *const VALUE, opt: ::std::os::raw::c_int, ) -> VALUE; + pub fn rb_obj_info(obj: VALUE) -> *const ::std::os::raw::c_char; + pub fn rb_ec_stack_check(ec: *mut rb_execution_context_struct) -> ::std::os::raw::c_int; pub fn rb_ary_tmp_new_from_values( arg1: VALUE, arg2: ::std::os::raw::c_long, @@ -1108,6 +1110,14 @@ extern "C" { n: ::std::os::raw::c_long, elts: *const VALUE, ) -> VALUE; + pub fn rb_shape_id_offset() -> i32; + pub fn rb_obj_shape_id(obj: VALUE) -> shape_id_t; + pub fn rb_shape_get_iv_index(shape_id: shape_id_t, id: ID, value: *mut attr_index_t) -> bool; + pub fn rb_shape_transition_add_ivar_no_warnings( + shape_id: shape_id_t, + id: ID, + klass: VALUE, + ) -> shape_id_t; pub fn rb_vm_top_self() -> VALUE; pub static mut rb_vm_insn_count: u64; pub fn rb_method_entry_at(obj: VALUE, id: ID) -> *const rb_method_entry_t; @@ -1126,16 +1136,6 @@ extern "C" { pub fn rb_vm_frame_method_entry( cfp: *const rb_control_frame_t, ) -> *const rb_callable_method_entry_t; - pub fn rb_obj_info(obj: VALUE) -> *const ::std::os::raw::c_char; - pub fn rb_ec_stack_check(ec: *mut rb_execution_context_struct) -> ::std::os::raw::c_int; - pub fn rb_shape_id_offset() -> i32; - pub fn rb_obj_shape_id(obj: VALUE) -> shape_id_t; - pub fn rb_shape_get_iv_index(shape_id: shape_id_t, id: ID, value: *mut attr_index_t) -> bool; - pub fn rb_shape_transition_add_ivar_no_warnings( - shape_id: shape_id_t, - id: ID, - klass: VALUE, - ) -> shape_id_t; pub fn rb_ivar_get_at(obj: VALUE, index: attr_index_t, id: ID) -> VALUE; pub fn rb_ivar_get_at_no_ractor_check(obj: VALUE, index: attr_index_t) -> VALUE; pub fn rb_gvar_get(arg1: ID) -> VALUE; diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 5bce8574e81e87..3e84f0d4eb5a7f 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -215,6 +215,7 @@ pub const ONIG_OPTION_EXTEND: u32 = 2; pub const ONIG_OPTION_MULTILINE: u32 = 4; pub const ARG_ENCODING_FIXED: u32 = 16; pub const ARG_ENCODING_NONE: u32 = 32; +pub const SHAPE_ID_NUM_BITS: u32 = 32; pub const INTEGER_REDEFINED_OP_FLAG: u32 = 1; pub const FLOAT_REDEFINED_OP_FLAG: u32 = 2; pub const STRING_REDEFINED_OP_FLAG: u32 = 4; @@ -233,7 +234,6 @@ pub const VM_ENV_DATA_INDEX_ME_CREF: i32 = -2; pub const VM_ENV_DATA_INDEX_SPECVAL: i32 = -1; pub const VM_ENV_DATA_INDEX_FLAGS: u32 = 0; pub const VM_BLOCK_HANDLER_NONE: u32 = 0; -pub const SHAPE_ID_NUM_BITS: u32 = 32; pub const ZJIT_STACK_MAP_VREG_TAG: u32 = 8; pub const ZJIT_STACK_MAP_SKIP_TAG: u32 = 16; pub const ZJIT_STACK_MAP_SHIFT: u32 = 8; @@ -403,6 +403,60 @@ pub const RUBY_ENCINDEX_EUC_JP: ruby_preserved_encindex = 10; pub const RUBY_ENCINDEX_Windows_31J: ruby_preserved_encindex = 11; pub const RUBY_ENCINDEX_BUILTIN_MAX: ruby_preserved_encindex = 12; pub type ruby_preserved_encindex = u32; +pub type rb_atomic_t = ::std::os::raw::c_uint; +#[repr(C)] +#[repr(align(8))] +#[derive(Debug, Copy, Clone)] +pub struct rb_id_table { + pub _bindgen_opaque_blob: [u64; 3usize], +} +pub const imemo_env: imemo_type = 0; +pub const imemo_cref: imemo_type = 1; +pub const imemo_svar: imemo_type = 2; +pub const imemo_throw_data: imemo_type = 3; +pub const imemo_ifunc: imemo_type = 4; +pub const imemo_memo: imemo_type = 5; +pub const imemo_ment: imemo_type = 6; +pub const imemo_iseq: imemo_type = 7; +pub const imemo_tmpbuf: imemo_type = 8; +pub const imemo_cvar_entry: imemo_type = 9; +pub const imemo_callinfo: imemo_type = 10; +pub const imemo_callcache: imemo_type = 11; +pub const imemo_constcache: imemo_type = 12; +pub const imemo_fields: imemo_type = 13; +pub const imemo_subclasses: imemo_type = 14; +pub const imemo_cdhash: imemo_type = 15; +pub type imemo_type = u32; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct vm_ifunc_argc { + pub min: ::std::os::raw::c_int, + pub max: ::std::os::raw::c_int, +} +#[repr(C)] +pub struct vm_ifunc { + pub flags: VALUE, + pub svar_lep: *mut VALUE, + pub func: rb_block_call_func_t, + pub data: *const ::std::os::raw::c_void, + pub argc: vm_ifunc_argc, +} +pub type attr_index_t = u8; +pub type shape_id_t = u32; +pub const SHAPE_ID_CAPACITY_MASK: shape_id_fl_type = 66584576; +pub const SHAPE_ID_FL_COMPLEX: shape_id_fl_type = 67108864; +pub const SHAPE_ID_FL_FROZEN: shape_id_fl_type = 134217728; +pub const SHAPE_ID_FL_HAS_OBJECT_ID: shape_id_fl_type = 268435456; +pub const SHAPE_ID_LAYOUT_ROBJECT: shape_id_fl_type = 0; +pub const SHAPE_ID_LAYOUT_RCLASS: shape_id_fl_type = 536870912; +pub const SHAPE_ID_LAYOUT_EXTENDED: shape_id_fl_type = 1073741824; +pub const SHAPE_ID_LAYOUT_RDATA: shape_id_fl_type = 1073741824; +pub const SHAPE_ID_LAYOUT_OTHER: shape_id_fl_type = 1610612736; +pub const SHAPE_ID_LAYOUT_MASK: shape_id_fl_type = 1610612736; +pub const SHAPE_ID_FL_NON_CANONICAL_MASK: shape_id_fl_type = 402653184; +pub const SHAPE_ID_FLAGS_MASK: shape_id_fl_type = 2146959360; +pub const SHAPE_ID_FL_PRIVATE_MASK: shape_id_fl_type = 1677197312; +pub type shape_id_fl_type = u32; pub const BOP_PLUS: ruby_basic_operators = 0; pub const BOP_MINUS: ruby_basic_operators = 1; pub const BOP_MULT: ruby_basic_operators = 2; @@ -442,44 +496,6 @@ pub const BOP_YIELD: ruby_basic_operators = 35; pub const BOP_LAST_: ruby_basic_operators = 36; pub type ruby_basic_operators = u32; pub type rb_serial_t = ::std::os::raw::c_ulonglong; -#[repr(C)] -#[repr(align(8))] -#[derive(Debug, Copy, Clone)] -pub struct rb_id_table { - pub _bindgen_opaque_blob: [u64; 3usize], -} -pub const imemo_env: imemo_type = 0; -pub const imemo_cref: imemo_type = 1; -pub const imemo_svar: imemo_type = 2; -pub const imemo_throw_data: imemo_type = 3; -pub const imemo_ifunc: imemo_type = 4; -pub const imemo_memo: imemo_type = 5; -pub const imemo_ment: imemo_type = 6; -pub const imemo_iseq: imemo_type = 7; -pub const imemo_tmpbuf: imemo_type = 8; -pub const imemo_cvar_entry: imemo_type = 9; -pub const imemo_callinfo: imemo_type = 10; -pub const imemo_callcache: imemo_type = 11; -pub const imemo_constcache: imemo_type = 12; -pub const imemo_fields: imemo_type = 13; -pub const imemo_subclasses: imemo_type = 14; -pub const imemo_cdhash: imemo_type = 15; -pub type imemo_type = u32; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct vm_ifunc_argc { - pub min: ::std::os::raw::c_int, - pub max: ::std::os::raw::c_int, -} -#[repr(C)] -pub struct vm_ifunc { - pub flags: VALUE, - pub svar_lep: *mut VALUE, - pub func: rb_block_call_func_t, - pub data: *const ::std::os::raw::c_void, - pub argc: vm_ifunc_argc, -} -pub type rb_atomic_t = ::std::os::raw::c_uint; pub const METHOD_VISI_UNDEF: rb_method_visibility_t = 0; pub const METHOD_VISI_PUBLIC: rb_method_visibility_t = 1; pub const METHOD_VISI_PRIVATE: rb_method_visibility_t = 2; @@ -1541,22 +1557,6 @@ pub const VM_ENV_FLAG_ESCAPED: vm_frame_env_flags = 4; pub const VM_ENV_FLAG_WB_REQUIRED: vm_frame_env_flags = 8; pub const VM_ENV_FLAG_ISOLATED: vm_frame_env_flags = 16; pub type vm_frame_env_flags = u32; -pub type attr_index_t = u8; -pub type shape_id_t = u32; -pub const SHAPE_ID_CAPACITY_MASK: shape_id_fl_type = 66584576; -pub const SHAPE_ID_FL_COMPLEX: shape_id_fl_type = 67108864; -pub const SHAPE_ID_FL_FROZEN: shape_id_fl_type = 134217728; -pub const SHAPE_ID_FL_HAS_OBJECT_ID: shape_id_fl_type = 268435456; -pub const SHAPE_ID_LAYOUT_ROBJECT: shape_id_fl_type = 0; -pub const SHAPE_ID_LAYOUT_RCLASS: shape_id_fl_type = 536870912; -pub const SHAPE_ID_LAYOUT_EXTENDED: shape_id_fl_type = 1073741824; -pub const SHAPE_ID_LAYOUT_RDATA: shape_id_fl_type = 1073741824; -pub const SHAPE_ID_LAYOUT_OTHER: shape_id_fl_type = 1610612736; -pub const SHAPE_ID_LAYOUT_MASK: shape_id_fl_type = 1610612736; -pub const SHAPE_ID_FL_NON_CANONICAL_MASK: shape_id_fl_type = 402653184; -pub const SHAPE_ID_FLAGS_MASK: shape_id_fl_type = 2146959360; -pub const SHAPE_ID_FL_PRIVATE_MASK: shape_id_fl_type = 1677197312; -pub type shape_id_fl_type = u32; pub const CONST_DEPRECATED: rb_const_flag_t = 256; pub const CONST_VISIBILITY_MASK: rb_const_flag_t = 255; pub const CONST_PUBLIC: rb_const_flag_t = 0; @@ -2176,6 +2176,19 @@ unsafe extern "C" { elements: *const VALUE, opt: ::std::os::raw::c_int, ) -> VALUE; + pub fn rb_obj_info(obj: VALUE) -> *const ::std::os::raw::c_char; + pub fn rb_raw_obj_info( + buff: *mut ::std::os::raw::c_char, + buff_size: usize, + obj: VALUE, + ) -> *const ::std::os::raw::c_char; + pub fn rb_ec_stack_check(ec: *mut rb_execution_context_struct) -> ::std::os::raw::c_int; + pub fn rb_gc_writebarrier_remember(obj: VALUE); + pub fn rb_id_table_lookup( + tbl: *mut rb_id_table, + id: ID, + valp: *mut VALUE, + ) -> ::std::os::raw::c_int; pub fn rb_ary_tmp_new_from_values( arg1: VALUE, arg2: ::std::os::raw::c_long, @@ -2186,13 +2199,16 @@ unsafe extern "C" { n: ::std::os::raw::c_long, elts: *const VALUE, ) -> VALUE; + pub fn rb_shape_id_offset() -> i32; + pub fn rb_obj_shape_id(obj: VALUE) -> shape_id_t; + pub fn rb_shape_get_iv_index(shape_id: shape_id_t, id: ID, value: *mut attr_index_t) -> bool; + pub fn rb_shape_transition_add_ivar_no_warnings( + shape_id: shape_id_t, + id: ID, + klass: VALUE, + ) -> shape_id_t; pub fn rb_vm_top_self() -> VALUE; pub static mut rb_vm_insn_count: u64; - pub fn rb_id_table_lookup( - tbl: *mut rb_id_table, - id: ID, - valp: *mut VALUE, - ) -> ::std::os::raw::c_int; pub fn rb_method_entry_at(obj: VALUE, id: ID) -> *const rb_method_entry_t; pub fn rb_callable_method_entry(klass: VALUE, id: ID) -> *const rb_callable_method_entry_t; pub fn rb_callable_method_entry_or_negative( @@ -2210,22 +2226,6 @@ unsafe extern "C" { pub fn rb_vm_frame_method_entry( cfp: *const rb_control_frame_t, ) -> *const rb_callable_method_entry_t; - pub fn rb_obj_info(obj: VALUE) -> *const ::std::os::raw::c_char; - pub fn rb_raw_obj_info( - buff: *mut ::std::os::raw::c_char, - buff_size: usize, - obj: VALUE, - ) -> *const ::std::os::raw::c_char; - pub fn rb_ec_stack_check(ec: *mut rb_execution_context_struct) -> ::std::os::raw::c_int; - pub fn rb_gc_writebarrier_remember(obj: VALUE); - pub fn rb_shape_id_offset() -> i32; - pub fn rb_obj_shape_id(obj: VALUE) -> shape_id_t; - pub fn rb_shape_get_iv_index(shape_id: shape_id_t, id: ID, value: *mut attr_index_t) -> bool; - pub fn rb_shape_transition_add_ivar_no_warnings( - shape_id: shape_id_t, - id: ID, - klass: VALUE, - ) -> shape_id_t; pub fn rb_const_lookup(klass: VALUE, id: ID) -> *mut rb_const_entry_t; pub fn rb_ivar_get_at_no_ractor_check(obj: VALUE, index: attr_index_t) -> VALUE; pub fn rb_gvar_get(arg1: ID) -> VALUE; From 8f4a3c94016dc80979fd0811be0dc43528d832f4 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 20 Jul 2026 17:29:01 +0900 Subject: [PATCH 2/9] [Feature #22215] Extract internal coverage API Provide a narrow coverage interface so `ext/coverage` does not depend on GC or VM implementation headers. --- compile.c | 1 + ext/coverage/coverage.c | 84 ++++++++++++----------------------------- internal/coverage.h | 50 ++++++++++++++++++++++++ internal/thread.h | 12 ------ iseq.c | 1 + iseq.h | 1 + ruby.c | 1 + thread.c | 61 ++++++++++++++++++++++++++++++ vm_core.h | 7 ---- 9 files changed, 140 insertions(+), 78 deletions(-) create mode 100644 internal/coverage.h diff --git a/compile.c b/compile.c index a6a498e9a1f518..adc0037b8dbf79 100644 --- a/compile.c +++ b/compile.c @@ -21,6 +21,7 @@ #include "internal.h" #include "internal/array.h" #include "internal/compile.h" +#include "internal/coverage.h" #include "internal/complex.h" #include "internal/encoding.h" #include "internal/error.h" diff --git a/ext/coverage/coverage.c b/ext/coverage/coverage.c index eae5ddfec8b638..95688c309b0316 100644 --- a/ext/coverage/coverage.c +++ b/ext/coverage/coverage.c @@ -8,12 +8,9 @@ ************************************************/ -#include "internal/gc.h" +#include "internal/coverage.h" #include "internal/hash.h" -#include "internal/thread.h" -#include "internal/sanitizers.h" #include "ruby.h" -#include "vm_core.h" static enum { IDLE, @@ -42,13 +39,13 @@ rb_coverage_supported(VALUE self, VALUE _mode) { ID mode = RB_SYM2ID(_mode); - return RBOOL( + return ( mode == rb_intern("lines") || mode == rb_intern("oneshot_lines") || mode == rb_intern("branches") || mode == rb_intern("methods") || mode == rb_intern("eval") - ); + ) ? Qtrue : Qfalse; } /* @@ -242,60 +239,30 @@ branch_coverage(VALUE branches) return b.result; } -/* - * Fold a single method entry's contribution into the method coverage result. - * `add` is the number of calls to attribute to it (0 is used to make sure a - * defined-but-uncalled method shows up). Entries that resolve elsewhere (e.g. - * aliases) are skipped: their calls are attributed to the original definition - * via the [owner, mid, location] key, exactly like the old heap-walk did. - */ static void -method_coverage_add(const rb_method_entry_t *me, long add, VALUE ncoverages) +method_coverage_i(const struct rb_coverage_method_data *method, void *data) { - VALUE data[5]; - const rb_method_entry_t *me2 = rb_resolve_me_location(me, data); - if (me != me2) return; - - VALUE klass = me->owner; - if (RB_TYPE_P(klass, T_ICLASS)) return; - - VALUE first_lineno = data[1]; - if (FIX2LONG(first_lineno) <= 0) return; - - VALUE path = data[0]; - VALUE ncoverage = rb_hash_aref(ncoverages, path); - if (NIL_P(ncoverage)) return; - - VALUE methods = rb_hash_aref(ncoverage, ID2SYM(rb_intern("methods"))); - VALUE method_id = ID2SYM(me->def->original_id); - VALUE key = rb_ary_new_from_args(6, klass, method_id, data[1], data[2], data[3], data[4]); - - VALUE rcount = rb_hash_aref(methods, key); - long count = NIL_P(rcount) ? 0 : FIX2LONG(rcount); - if (!POSFIXABLE(count + add)) { - count = FIXNUM_MAX; - } - else { - count += add; + VALUE ncoverages = *(VALUE *)data; + VALUE ncoverage = rb_hash_aref(ncoverages, method->path); + + if (!NIL_P(ncoverage)) { + VALUE methods = rb_hash_aref(ncoverage, ID2SYM(rb_intern("methods"))); + VALUE key = rb_ary_new_from_args(6, method->owner, method->method_id, + method->first_lineno, method->first_column, + method->last_lineno, method->last_column); + VALUE rcount = method->count; + VALUE previous = rb_hash_aref(methods, key); + + if (NIL_P(rcount)) rcount = LONG2FIX(0); + if (NIL_P(previous)) previous = LONG2FIX(0); + if (!POSFIXABLE(FIX2LONG(rcount) + FIX2LONG(previous))) { + rcount = LONG2FIX(FIXNUM_MAX); + } + else { + rcount = LONG2FIX(FIX2LONG(rcount) + FIX2LONG(previous)); + } + rb_hash_aset(methods, key, rcount); } - rb_hash_aset(methods, key, LONG2FIX(count)); -} - -/* me_set entries: every defined method, so uncalled ones appear with count 0. */ -static int -method_coverage_me_i(VALUE key, VALUE value, VALUE ncoverages) -{ - method_coverage_add((const rb_method_entry_t *)key, 0, ncoverages); - return ST_CONTINUE; -} - -/* cme2counter entries: call counts, attributed to the definition's key. */ -static int -method_coverage_count_i(VALUE key, VALUE value, VALUE ncoverages) -{ - long add = FIXNUM_P(value) ? FIX2LONG(value) : 0; - method_coverage_add((const rb_method_entry_t *)key, add, ncoverages); - return ST_CONTINUE; } static int @@ -361,8 +328,7 @@ rb_coverage_peek_result(VALUE klass) rb_hash_foreach(coverages, coverage_peek_result_i, ncoverages); if (current_mode & COVERAGE_TARGET_METHODS) { - if (RTEST(me_set)) rb_hash_foreach(me_set, method_coverage_me_i, ncoverages); - if (RTEST(cme2counter)) rb_hash_foreach(cme2counter, method_coverage_count_i, ncoverages); + rb_coverage_each_method(method_coverage_i, &ncoverages); } rb_hash_freeze(ncoverages); diff --git a/internal/coverage.h b/internal/coverage.h new file mode 100644 index 00000000000000..9c78661671dc23 --- /dev/null +++ b/internal/coverage.h @@ -0,0 +1,50 @@ +#ifndef INTERNAL_COVERAGE_H /*-*-C-*-vi:se ft=c:*/ +#define INTERNAL_COVERAGE_H +/** + * @author Ruby developers + * @copyright This file is a part of the programming language Ruby. + * Permission is hereby granted, to either redistribute and/or + * modify this file, provided that the conditions mentioned in the + * file COPYING are met. Consult the file for details. + * @brief Internal header for Coverage. + */ +#include "ruby/ruby.h" + +#define COVERAGE_INDEX_LINES 0 +#define COVERAGE_INDEX_BRANCHES 1 + +#define COVERAGE_TARGET_LINES 1 +#define COVERAGE_TARGET_BRANCHES 2 +#define COVERAGE_TARGET_METHODS 4 +#define COVERAGE_TARGET_ONESHOT_LINES 8 +#define COVERAGE_TARGET_EVAL 16 + +struct rb_coverage_method_data { + VALUE owner; + VALUE method_id; + VALUE path; + VALUE first_lineno; + VALUE first_column; + VALUE last_lineno; + VALUE last_column; + VALUE count; +}; + +typedef void rb_coverage_method_callback(const struct rb_coverage_method_data *, void *); + +RUBY_SYMBOL_EXPORT_BEGIN + +VALUE rb_get_coverages(void); +void rb_set_coverages(VALUE coverages, int mode, VALUE cme2counter, VALUE me_set); +void rb_clear_coverages(void); +void rb_reset_coverages(void); +void rb_resume_coverages(void); +void rb_suspend_coverages(void); +void rb_coverage_each_method(rb_coverage_method_callback callback, void *data); + +RUBY_SYMBOL_EXPORT_END + +int rb_get_coverage_mode(void); +VALUE rb_default_coverage(int n); + +#endif /* INTERNAL_COVERAGE_H */ diff --git a/internal/thread.h b/internal/thread.h index ad5b645c932dae..77226dafdbebda 100644 --- a/internal/thread.h +++ b/internal/thread.h @@ -23,15 +23,6 @@ struct rb_io; SET_MACHINE_STACK_END(&(th)->ec->machine.stack_end); \ } while (0) -/* thread.c */ -#define COVERAGE_INDEX_LINES 0 -#define COVERAGE_INDEX_BRANCHES 1 -#define COVERAGE_TARGET_LINES 1 -#define COVERAGE_TARGET_BRANCHES 2 -#define COVERAGE_TARGET_METHODS 4 -#define COVERAGE_TARGET_ONESHOT_LINES 8 -#define COVERAGE_TARGET_EVAL 16 - #define RUBY_FATAL_THREAD_KILLED INT2FIX(0) #define RUBY_FATAL_THREAD_TERMINATED INT2FIX(1) #define RUBY_FATAL_FIBER_KILLED RB_INT2FIX(2) @@ -39,9 +30,6 @@ struct rb_io; VALUE rb_obj_is_mutex(VALUE obj); VALUE rb_suppress_tracing(VALUE (*func)(VALUE), VALUE arg); void rb_thread_execute_interrupts(VALUE th); -VALUE rb_get_coverages(void); -int rb_get_coverage_mode(void); -VALUE rb_default_coverage(int); VALUE rb_thread_shield_new(void); bool rb_thread_shield_owned(VALUE self); VALUE rb_thread_shield_wait(VALUE self); diff --git a/iseq.c b/iseq.c index 23e9abcea7be9b..a51ad14439233e 100644 --- a/iseq.c +++ b/iseq.c @@ -25,6 +25,7 @@ #include "internal/bits.h" #include "internal/class.h" #include "internal/compile.h" +#include "internal/coverage.h" #include "internal/error.h" #include "internal/file.h" #include "internal/gc.h" diff --git a/iseq.h b/iseq.h index 8308260fedfd68..c9bdfcb484759f 100644 --- a/iseq.h +++ b/iseq.h @@ -10,6 +10,7 @@ Copyright (C) 2004-2008 Koichi Sasada **********************************************************************/ +#include "internal/coverage.h" #include "internal/gc.h" #include "shape.h" #include "vm_core.h" diff --git a/ruby.c b/ruby.c index d83d83cf93380a..3d7ef4ff993968 100644 --- a/ruby.c +++ b/ruby.c @@ -45,6 +45,7 @@ #include "internal.h" #include "internal/cmdlineopt.h" #include "internal/cont.h" +#include "internal/coverage.h" #include "internal/error.h" #include "internal/file.h" #include "internal/inits.h" diff --git a/thread.c b/thread.c index 5f84c0399418da..68a16e4a1f2489 100644 --- a/thread.c +++ b/thread.c @@ -77,6 +77,7 @@ #include "internal.h" #include "internal/class.h" #include "internal/cont.h" +#include "internal/coverage.h" #include "internal/error.h" #include "internal/eval.h" #include "internal/gc.h" @@ -6099,6 +6100,66 @@ rb_resolve_me_location(const rb_method_entry_t *me, VALUE resolved_location[5]) return me; } +struct method_coverage_arg { + rb_coverage_method_callback *callback; + void *data; +}; + +static void +method_coverage_call(const rb_method_entry_t *me, VALUE count, + struct method_coverage_arg *arg) +{ + VALUE location[5]; + const rb_method_entry_t *resolved_me = rb_resolve_me_location(me, location); + + if (me != resolved_me || RB_TYPE_P(me->owner, T_ICLASS) || + FIX2LONG(location[1]) <= 0) return; + + struct rb_coverage_method_data method = { + .owner = me->owner, + .method_id = ID2SYM(me->def->original_id), + .path = location[0], + .first_lineno = location[1], + .first_column = location[2], + .last_lineno = location[3], + .last_column = location[4], + .count = count, + }; + arg->callback(&method, arg->data); +} + +static int +method_coverage_me_i(VALUE me, VALUE value, VALUE data) +{ + method_coverage_call((const rb_method_entry_t *)me, INT2FIX(0), + (struct method_coverage_arg *)data); + return ST_CONTINUE; +} + +static int +method_coverage_count_i(VALUE me, VALUE count, VALUE data) +{ + if (!FIXNUM_P(count)) count = INT2FIX(0); + method_coverage_call((const rb_method_entry_t *)me, count, + (struct method_coverage_arg *)data); + return ST_CONTINUE; +} + +void +rb_coverage_each_method(rb_coverage_method_callback callback, void *data) +{ + struct method_coverage_arg arg = {callback, data}; + VALUE me_set = GET_VM()->me_set; + VALUE cme2counter = GET_VM()->cme2counter; + + if (RTEST(me_set)) { + rb_hash_foreach(me_set, method_coverage_me_i, (VALUE)&arg); + } + if (RTEST(cme2counter)) { + rb_hash_foreach(cme2counter, method_coverage_count_i, (VALUE)&arg); + } +} + static void update_method_coverage(VALUE cme2counter, rb_trace_arg_t *trace_arg) { diff --git a/vm_core.h b/vm_core.h index ce118d5080ccbf..e82227aa6414eb 100644 --- a/vm_core.h +++ b/vm_core.h @@ -2446,13 +2446,6 @@ int rb_thread_check_trap_pending(void); #define RUBY_EVENT_COVERAGE_LINE 0x010000 #define RUBY_EVENT_COVERAGE_BRANCH 0x020000 -extern VALUE rb_get_coverages(void); -extern void rb_set_coverages(VALUE, int, VALUE, VALUE); -extern void rb_clear_coverages(void); -extern void rb_reset_coverages(void); -extern void rb_resume_coverages(void); -extern void rb_suspend_coverages(void); - void rb_postponed_job_flush(rb_vm_t *vm); void rb_postponed_job_trigger_for_ractor(unsigned int h, VALUE running_ractor); From b68ef95a51a919a84510e84641136b07b514e175 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 20 Jul 2026 17:29:01 +0900 Subject: [PATCH 3/9] [Feature #22215] Extract internal objspace API Provide narrow accessors so `ext/objspace` does not depend on call cache and call info layouts. --- ext/objspace/object_tracing.c | 1 - ext/objspace/objspace.c | 8 ++------ ext/objspace/objspace_dump.c | 19 +++++++------------ imemo.c | 21 +++++++++++++++++++++ internal/imemo.h | 6 ++++++ internal/objspace.h | 24 ++++++++++++++++++++++++ 6 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 internal/objspace.h diff --git a/ext/objspace/object_tracing.c b/ext/objspace/object_tracing.c index 0497c89210e6f5..93f631ec1ce12a 100644 --- a/ext/objspace/object_tracing.c +++ b/ext/objspace/object_tracing.c @@ -13,7 +13,6 @@ **********************************************************************/ -#include "internal.h" #include "internal/gc.h" #include "ruby/debug.h" #include "objspace.h" diff --git a/ext/objspace/objspace.c b/ext/objspace/objspace.c index 0f0470ed637ecf..35d38bfd87791a 100644 --- a/ext/objspace/objspace.c +++ b/ext/objspace/objspace.c @@ -13,20 +13,16 @@ **********************************************************************/ #include "internal.h" -#include "internal/class.h" -#include "internal/compilers.h" #include "internal/gc.h" #include "internal/hash.h" #include "internal/imemo.h" +#include "internal/objspace.h" #include "internal/sanitizers.h" #include "ruby/io.h" #include "ruby/re.h" #include "ruby/st.h" #include "symbol.h" - -#undef rb_funcall - -#include "ruby/ruby.h" +#include "objspace.h" /* * call-seq: diff --git a/ext/objspace/objspace_dump.c b/ext/objspace/objspace_dump.c index e7778d08dd6214..7bd34864476c47 100644 --- a/ext/objspace/objspace_dump.c +++ b/ext/objspace/objspace_dump.c @@ -14,24 +14,18 @@ #include "id_table.h" #include "internal.h" -#include "internal/array.h" -#include "internal/class.h" -#include "internal/gc.h" #include "internal/hash.h" +#include "internal/imemo.h" #include "internal/io.h" +#include "internal/objspace.h" #include "internal/string.h" #include "internal/sanitizers.h" #include "symbol.h" #include "shape.h" -#include "node.h" #include "objspace.h" #include "ruby/debug.h" #include "ruby/util.h" #include "ruby/io.h" -#include "vm_callinfo.h" -#include "vm_sync.h" - -RUBY_EXTERN const char ruby_hexdigits[]; #define BUFFER_CAPACITY 4096 @@ -443,7 +437,7 @@ dump_object(VALUE obj, struct dump_config *dc) switch (imemo_type(obj)) { case imemo_callinfo: - mid = vm_ci_mid((const struct rb_callinfo *)obj); + mid = rb_imemo_callinfo_mid(obj); if (mid != 0) { dump_append(dc, ", \"mid\":"); dump_append_id(dc, mid); @@ -452,9 +446,10 @@ dump_object(VALUE obj, struct dump_config *dc) case imemo_callcache: { - VALUE klass = ((const struct rb_callcache *)obj)->klass; - if (klass != Qundef) { - mid = vm_cc_cme((const struct rb_callcache *)obj)->called_id; + struct rb_imemo_callcache_data data; + if (rb_imemo_callcache_get_data(obj, &data)) { + VALUE klass = data.klass; + mid = data.called_id; if (mid != 0) { dump_append(dc, ", \"called_id\":"); dump_append_id(dc, mid); diff --git a/imemo.c b/imemo.c index 62f9e5768dd28d..5fb5766a55eb96 100644 --- a/imemo.c +++ b/imemo.c @@ -11,6 +11,27 @@ size_t rb_iseq_memsize(const rb_iseq_t *iseq); void rb_iseq_mark_and_move(rb_iseq_t *iseq, bool reference_updating); void rb_iseq_free(const rb_iseq_t *iseq); +ID +rb_imemo_callinfo_mid(VALUE obj) +{ + RUBY_ASSERT(imemo_type(obj) == imemo_callinfo); + return vm_ci_mid((const struct rb_callinfo *)obj); +} + +bool +rb_imemo_callcache_get_data(VALUE obj, struct rb_imemo_callcache_data *data) +{ + const struct rb_callcache *cc = (const struct rb_callcache *)obj; + + RUBY_ASSERT(imemo_type(obj) == imemo_callcache); + + if (cc->klass == Qundef) return false; + + data->klass = cc->klass; + data->called_id = vm_cc_cme(cc)->called_id; + return true; +} + const char * rb_imemo_name(enum imemo_type type) { diff --git a/internal/imemo.h b/internal/imemo.h index da8196f47167c7..fee5c07bc33703 100644 --- a/internal/imemo.h +++ b/internal/imemo.h @@ -160,6 +160,12 @@ void rb_imemo_free(VALUE obj); RUBY_SYMBOL_EXPORT_BEGIN const char *rb_imemo_name(enum imemo_type type); +ID rb_imemo_callinfo_mid(VALUE obj); +struct rb_imemo_callcache_data { + VALUE klass; + ID called_id; +}; +bool rb_imemo_callcache_get_data(VALUE obj, struct rb_imemo_callcache_data *data); RUBY_SYMBOL_EXPORT_END static inline enum imemo_type diff --git a/internal/objspace.h b/internal/objspace.h new file mode 100644 index 00000000000000..db471bda17b3b9 --- /dev/null +++ b/internal/objspace.h @@ -0,0 +1,24 @@ +#ifndef INTERNAL_OBJSPACE_H /*-*-C-*-vi:se ft=c:*/ +#define INTERNAL_OBJSPACE_H +/** + * @author Ruby developers + * @copyright This file is a part of the programming language Ruby. + * Permission is hereby granted, to either redistribute and/or + * modify this file, provided that the conditions mentioned in the + * file COPYING are met. Consult the file for details. + * @brief Internal header for Objspace. + */ + +RUBY_SYMBOL_EXPORT_BEGIN + +/* from intern/class.h */ +RUBY_EXTERN VALUE rb_class_super_of(VALUE klass); +RUBY_EXTERN VALUE rb_class_singleton_p(VALUE klass); +RUBY_EXTERN unsigned char rb_class_variation_count(VALUE klass); + +/* from vm_sync.h */ +RUBY_EXTERN VALUE rb_vm_lock_with_barrier(VALUE (*func)(void *args), void *args); + +RUBY_SYMBOL_EXPORT_END + +#endif /* INTERNAL_OBJSPACE_H */ From da7354d6bab7f7b024d0002673ccb96ff6a06724 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 02:12:33 +0000 Subject: [PATCH 4/9] Bump the github-actions group across 1 directory with 4 updates Bumps the github-actions group with 4 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action), [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) and [taiki-e/install-action](https://github.com/taiki-e/install-action). Updates `github/codeql-action/init` from 4.37.5 to 4.37.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/d1ba80a13dd99fba24a470575428917156a28b43...5595ccaf912efad79be6eef63a5619ff05969be3) Updates `github/codeql-action/analyze` from 4.37.5 to 4.37.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/d1ba80a13dd99fba24a470575428917156a28b43...5595ccaf912efad79be6eef63a5619ff05969be3) Updates `github/codeql-action/upload-sarif` from 4.37.5 to 4.37.6 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/d1ba80a13dd99fba24a470575428917156a28b43...5595ccaf912efad79be6eef63a5619ff05969be3) Updates `taiki-e/install-action` from 2.85.7 to 2.85.8 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/67729d5c413db75907f0ad1e39bb04b9c868ff60...cb33e69fad06166ca28a42b2575e4dadabf62ee8) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.37.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.37.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.37.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: taiki-e/install-action dependency-version: 2.85.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/check_sast.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 9e8020ca6d7766..af35f0a5a59928 100644 --- a/.github/workflows/check_sast.yml +++ b/.github/workflows/check_sast.yml @@ -78,14 +78,14 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: languages: ${{ matrix.language }} build-mode: none config-file: .github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: category: '/language:${{ matrix.language }}' upload: False @@ -127,7 +127,7 @@ jobs: continue-on-error: true - name: Upload SARIF - uses: github/codeql-action/upload-sarif@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: sarif_file: sarif-results/${{ matrix.language }}.sarif continue-on-error: true diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index e59ba6bc55c9a7..1f7e23275a0c4e 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -73,6 +73,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: sarif_file: results.sarif diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 1e63e8b3115c00..28752a2584a199 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -98,7 +98,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2.85.7 + - uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index bac24c26439cbf..e4480f3f355249 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -152,7 +152,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2.85.7 + - uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From b3a2792b41f78b71d1cc0283e9138261b5b3fd0d Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 5 Aug 2026 09:42:42 -0700 Subject: [PATCH 5/9] ZJIT: Summarize the invokeblock block handler profile once Compute a TypeDistributionSummary for the profiled block handler distribution up front instead of deriving only the monomorphic class from it, so that upcoming polymorphic invokeblock specialization can reuse the same summary. No behavior change. --- zjit/src/hir.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index ac5cda92de1fdb..7ffdb5cca1d0a2 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -9510,12 +9510,17 @@ fn add_iseq_to_hir( } let args = state.stack_pop_n(crate::profile::num_arguments_on_stack(cd))?; - // The monomorphic block handler class the profile recorded, if any. Both the - // IFUNC and inline-ISEQ specializations below key off this single distribution. - let block_handler_types = payload.profile.get_operand_types(exit_state.insn_idx); - let block_handler_class = block_handler_types.and_then(|types| { - if types.len() != 1 { return None; } - let summary = TypeDistributionSummary::new(&types[0]); + // The profiled block handler distribution. All the specializations below + // (IFUNC, inline-ISEQ, and polymorphic ISEQ dispatch) key off this summary. + let block_handler_summary = payload.profile.get_operand_types(exit_state.insn_idx).and_then(|types| { + if let [block_handler_distribution] = types { + Some(TypeDistributionSummary::new(block_handler_distribution)) + } else { + None + } + }); + // The monomorphic block handler class the profile recorded, if any. + let block_handler_class = block_handler_summary.as_ref().and_then(|summary| { if !summary.is_monomorphic() { return None; } Some(summary.bucket(0).class()) }); From 9c2c8caf28828ecb0302f8b9132ee31ac6f8af29 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 5 Aug 2026 09:52:26 -0700 Subject: [PATCH 6/9] ZJIT: Compile polymorphic ISEQ invokeblock --- zjit/src/codegen_tests.rs | 41 ++++++++++++++++++++ zjit/src/hir.rs | 82 +++++++++++++++++++++++++++++++++++++++ zjit/src/hir/opt_tests.rs | 63 ++++++++++++++++++++++++++++++ zjit/src/stats.rs | 2 + 4 files changed, 188 insertions(+) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 33f82548288901..56cc6b026c7c49 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -595,6 +595,47 @@ fn test_yield_iseq_guard_miss_recompiles() { assert_snapshot!(assert_compiles_allowing_exits("[invoke { |x| x + 1 }, invoke { |x| x * 2 }]"), @"[42, 82]"); } +#[test] +fn test_yield_polymorphic_blocks_dispatch_directly() { + // A yield site shared by two call sites recompiles with a polymorphic ISEQ dispatch + // chain after the monomorphic guard miss. Once the polymorphic version is installed, + // both blocks must dispatch directly with no side exits. + set_call_threshold(2); + eval(" + def invoke = yield(10) + def add_one = invoke { |x| x + 1 } + def double = invoke { |x| x * 2 } + add_one; double + add_one; double + "); + // Drive the re-profile window so the invalidated monomorphic version is replaced. + let num_profiles = get_option!(num_profiles); + for _ in 0..num_profiles + 2 { + eval("add_one; double"); + } + assert_snapshot!(assert_compiles("[add_one, double]"), @"[11, 20]"); +} + +#[test] +fn test_yield_polymorphic_non_iseq_handler_falls_back() { + // A proc handler at a polymorphic yield site fails the ISEQ tag check and takes the + // generic InvokeBlock fallback in-line, without a side exit or another recompile. + set_call_threshold(2); + eval(" + def invoke = yield(10) + def add_one = invoke { |x| x + 1 } + def double = invoke { |x| x * 2 } + def via_proc(l) = invoke(&l) + add_one; double + add_one; double + "); + let num_profiles = get_option!(num_profiles); + for _ in 0..num_profiles + 2 { + eval("add_one; double; via_proc(proc { |x| x * 3 })"); + } + assert_snapshot!(assert_compiles("[add_one, double, via_proc(proc { |x| x * 3 })]"), @"[11, 20, 30]"); +} + #[test] fn test_yield_inline_invocation_with_args() { // Plain yield with two args to a matching-arity block inlines and returns correctly. diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 7ffdb5cca1d0a2..1887be5a6b8d0e 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -779,6 +779,9 @@ pub enum SendFallbackReason { SuperTargetComplexArgsPass, /// The `invokeblock` instruction is not yet optimized in `type_specialize`. InvokeBlockNotSpecialized, + /// The runtime block handler at a polymorphic `invokeblock` site did not match any + /// profiled ISEQ candidate, so the site dispatched through the generic fallback. + InvokeBlockPolymorphicMiss, /// The `sendforward` instruction (argument forwarding `...`) is not yet optimized in /// `type_specialize`. SendForwardNotSpecialized, @@ -830,6 +833,7 @@ impl Display for SendFallbackReason { SuperTargetNotFound => write!(f, "super: profiled target method cannot be found"), SuperTargetComplexArgsPass => write!(f, "super: complex argument passing to `super` target call"), InvokeBlockNotSpecialized => write!(f, "InvokeBlock: not yet specialized"), + InvokeBlockPolymorphicMiss => write!(f, "InvokeBlock: polymorphic dispatch miss"), SendForwardNotSpecialized => write!(f, "SendForward: not yet specialized"), InvokeSuperForwardNotSpecialized => write!(f, "InvokeSuperForward: not yet specialized"), SingleRactorModeRequired => write!(f, "Single-ractor mode required"), @@ -9544,6 +9548,29 @@ fn add_iseq_to_hir( }) } else { None }; + // For polymorphic yield sites, collect the profiled ISEQ blocks that can + // dispatch directly. Iterators like Integer#times are typically called with a + // different block per call site, so requiring a monomorphic profile would + // leave every such shared yield site on the generic fallback. Buckets are + // ordered by frequency, so the hottest block is compared first below. + let mut polymorphic_iseqs: Vec = vec![]; + if let Some(summary) = block_handler_summary.as_ref() { + if block_call_inlinable(flags) && (summary.is_polymorphic() || summary.is_skewed_polymorphic()) { + for &profiled_type in summary.buckets() { + if profiled_type.is_empty() { + break; + } + let obj = profiled_type.class(); + if unsafe { rb_IMEMO_TYPE_P(obj, imemo_iseq) == 1 } { + let iseq = obj.as_iseq(); + if !polymorphic_iseqs.contains(&iseq) && block_call_inlinable_iseq(iseq, args.len()).is_ok() { + polymorphic_iseqs.push(iseq); + } + } + } + } + } + let inlined_known_block = if let AddIseqMode::Inlined { blockiseq: Some(bi), .. } = mode { if block_call_inlinable(flags) // Only methods are inlined today, so exit_state.iseq is always a method iseq and this is @@ -9568,6 +9595,61 @@ fn add_iseq_to_hir( } else if let Some(block_iseq) = inline_iseq { let level = get_lvar_level(exit_state.iseq); fun.push_invoke_block_iseq_direct(block, block_iseq, level, args, exit_id, true) + } else if !polymorphic_iseqs.is_empty() { + // Dispatch on the runtime block ISEQ over the profiled candidates, joining + // on the generic fallback for anything else. Unlike the monomorphic path + // above, a miss must not side-exit: the site is known to see multiple + // blocks, so a guard would keep failing and recompiling. + let level = get_lvar_level(exit_state.iseq); + let ep = fun.get_ep(block, level); + let block_handler = fun.load_ep_env_field(block, ep, FieldName::VM_ENV_DATA_INDEX_SPECVAL, VM_ENV_DATA_INDEX_SPECVAL, types::CInt64); + + let join_block = fun.new_block(insn_idx); + let join_param = fun.push_insn(join_block, Insn::Param); + let dispatch_block = fun.new_block(insn_idx); + let fallback_block = fun.new_block(insn_idx); + + // The handler must be an ISEQ block: VM_BH_ISEQ_BLOCK_P is `& 0x3 == 0x1`. + let tag_mask = fun.push_insn(block, Insn::Const { val: Const::CInt64(0x3) }); + let tag = fun.push_insn(block, Insn::IntAnd { left: block_handler, right: tag_mask }); + let iseq_tag = fun.push_insn(block, Insn::Const { val: Const::CInt64(0x1) }); + let tag_matches = fun.push_insn(block, Insn::IsBitEqual { left: tag, right: iseq_tag }); + fun.push_insn(block, Insn::CondBranch { + val: tag_matches, + if_true: BranchEdge { target: dispatch_block, args: vec![] }, + if_false: BranchEdge { target: fallback_block, args: vec![] }, + }); + + // captured = block_handler & ~0x3 (struct rb_captured_block *) + let untag_mask = fun.push_insn(dispatch_block, Insn::Const { val: Const::CInt64(!0x3) }); + let captured = fun.push_insn(dispatch_block, Insn::IntAnd { left: block_handler, right: untag_mask }); + let captured_iseq = fun.load_field(dispatch_block, captured, FieldName::code_iseq, 2 * SIZEOF_VALUE_I32, types::CPtr); + + let mut compare_block = dispatch_block; + for &block_iseq in &polymorphic_iseqs { + let expected = fun.push_insn(compare_block, Insn::Const { val: Const::CPtr(block_iseq as *const u8) }); + let iseq_matches = fun.push_insn(compare_block, Insn::IsBitEqual { left: captured_iseq, right: expected }); + let direct_block = fun.new_block(insn_idx); + let miss_block = fun.new_block(insn_idx); + fun.push_insn(compare_block, Insn::CondBranch { + val: iseq_matches, + if_true: BranchEdge { target: direct_block, args: vec![] }, + if_false: BranchEdge { target: miss_block, args: vec![] }, + }); + let direct_result = fun.push_insn(direct_block, Insn::InvokeBlockIseqDirect { iseq: block_iseq, captured, args: args.clone(), state: exit_id }); + fun.push_insn(direct_block, Insn::Jump(BranchEdge { target: join_block, args: vec![direct_result] })); + compare_block = miss_block; + } + fun.push_insn(compare_block, Insn::Jump(BranchEdge { target: fallback_block, args: vec![] })); + + let fallback_result = fun.push_insn(fallback_block, Insn::InvokeBlock { + cd, args, state: exit_id, reason: InvokeBlockPolymorphicMiss, + }); + fun.push_insn(fallback_block, Insn::Jump(BranchEdge { target: join_block, args: vec![fallback_result] })); + + // Continue compilation from the join block + block = join_block; + join_param } else if is_ifunc { // Load the block handler from the current frame's LEP. In inlined // code, the function ISEQ is the caller while `exit_state.iseq` is the diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 2cf29df1c7b3b1..8cf0367a027f89 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -4053,6 +4053,69 @@ mod hir_opt_tests { "); } + #[test] + fn test_yield_polymorphic_blocks_dispatch_directly() { + // A yield site shared by multiple call sites (like Integer#times) profiles as + // polymorphic. Instead of falling back to the generic InvokeBlock, dispatch on + // the runtime block ISEQ over the profiled candidates, with the generic call + // only as the in-line miss path (no side exit). + let result = eval(" + def invoke = yield(10) + def add_one = invoke { |x| x + 1 } + def double = invoke { |x| x * 2 } + add_one; double + add_one; double + add_one; double + add_one + double + "); + assert_eq!(VALUE::fixnum_from_usize(31), result); + assert_snapshot!(hir_string("invoke"), @" + fn invoke@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:Fixnum[10] = Const Value(10) + v12:CPtr = GetEP 0 + v13:CInt64 = LoadField v12, :VM_ENV_DATA_INDEX_SPECVAL@0x1000 + v15:CInt64[3] = Const CInt64(3) + v16:CInt64 = IntAnd v13, v15 + v17:CInt64[1] = Const CInt64(1) + v18:CBool = IsBitEqual v16, v17 + CondBranch v18, bb5(), bb6() + bb5(): + v20:CInt64[-4] = Const CInt64(-4) + v21:CInt64 = IntAnd v13, v20 + v22:CPtr = LoadField v21, :code_iseq@0x1001 + v23:CPtr[CPtr(0x1002)] = Const CPtr(0x1002) + v24:CBool = IsBitEqual v22, v23 + CondBranch v24, bb7(), bb8() + bb7(): + v26:BasicObject = InvokeBlockIseqDirect (0x1002), v21, v10 + Jump bb4(v26) + bb8(): + v28:CPtr[CPtr(0x1003)] = Const CPtr(0x1003) + v29:CBool = IsBitEqual v22, v28 + CondBranch v29, bb9(), bb10() + bb9(): + v31:BasicObject = InvokeBlockIseqDirect (0x1003), v21, v10 + Jump bb4(v31) + bb10(): + Jump bb6() + bb6(): + v34:BasicObject = InvokeBlock v10 # SendFallbackReason: InvokeBlock: polymorphic dispatch miss + Jump bb4(v34) + bb4(v14:BasicObject): + CheckInterrupts + Return v14 + "); + } + #[test] fn reload_local_across_send() { eval(" diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 8347246f2b77af..4323545bd9111b 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -293,6 +293,7 @@ make_counters! { send_fallback_super_target_complex_args_pass, send_fallback_cannot_send_direct, send_fallback_invokeblock_not_specialized, + send_fallback_invokeblock_polymorphic_miss, send_fallback_sendforward_not_specialized, send_fallback_invokesuperforward_not_specialized, send_fallback_single_ractor_mode_required, @@ -705,6 +706,7 @@ pub fn send_fallback_counter(reason: crate::hir::SendFallbackReason) -> Counter SuperTargetNotFound => send_fallback_super_target_not_found, SuperTargetComplexArgsPass => send_fallback_super_target_complex_args_pass, InvokeBlockNotSpecialized => send_fallback_invokeblock_not_specialized, + InvokeBlockPolymorphicMiss => send_fallback_invokeblock_polymorphic_miss, SendForwardNotSpecialized => send_fallback_sendforward_not_specialized, InvokeSuperForwardNotSpecialized => send_fallback_invokesuperforward_not_specialized, SingleRactorModeRequired => send_fallback_single_ractor_mode_required, From 13a60fb4562a7abf5c99155bfcd9d2d7e7c5d6fa Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 6 Aug 2026 12:13:44 -0700 Subject: [PATCH 7/9] ZJIT: Use offset_of for the captured block code field Instead of hard-coding the 2 * SIZEOF_VALUE offset of rb_captured_block's code union, compute it with offset_of! from the bindgen-generated struct. --- zjit/src/hir.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 1887be5a6b8d0e..56ac49e6f6fad3 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -3002,6 +3002,12 @@ impl Function { self.load_field(block, str, FieldName::len, RUBY_OFFSET_RSTRING_LEN, types::CInt64) } + /// Load `captured->code.iseq` from a `struct rb_captured_block *`. + fn load_captured_code_iseq(&mut self, block: BlockId, captured: InsnId) -> InsnId { + let offset: i32 = std::mem::offset_of!(rb_captured_block, code).try_into().unwrap(); + self.load_field(block, captured, FieldName::code_iseq, offset, types::CPtr) + } + /// Emit the fast-path `yield` dispatch to a known ISEQ block. /// When `guarded`, the block handler is read from the runtime LEP and guarded (tag + iseq /// identity) because the profiled block can differ per caller. When the enclosing method is @@ -3025,7 +3031,7 @@ impl Function { if guarded { // Guard captured->code.iseq is the comptime block iseq. Compare the raw imemo pointer: // type inference (from_value) can't type an iseq imemo, so guard it as a CPtr identity. - let captured_iseq = self.load_field(block, captured, FieldName::code_iseq, 2 * SIZEOF_VALUE_I32, types::CPtr); + let captured_iseq = self.load_captured_code_iseq(block, captured); self.push_insn(block, Insn::GuardBitEquals { val: captured_iseq, expected: Const::CPtr(block_iseq as *const u8), reason: Box::new(SideExitReason::InvokeBlockIseqChanged), state, recompile: Some(Recompile) }); } @@ -9623,7 +9629,7 @@ fn add_iseq_to_hir( // captured = block_handler & ~0x3 (struct rb_captured_block *) let untag_mask = fun.push_insn(dispatch_block, Insn::Const { val: Const::CInt64(!0x3) }); let captured = fun.push_insn(dispatch_block, Insn::IntAnd { left: block_handler, right: untag_mask }); - let captured_iseq = fun.load_field(dispatch_block, captured, FieldName::code_iseq, 2 * SIZEOF_VALUE_I32, types::CPtr); + let captured_iseq = fun.load_captured_code_iseq(dispatch_block, captured); let mut compare_block = dispatch_block; for &block_iseq in &polymorphic_iseqs { From 60e3de26be955d1908dabd0ad66970acbd08c521 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 6 Aug 2026 12:30:31 -0700 Subject: [PATCH 8/9] ZJIT: Test mixed block handlers at polymorphic yield sites Cover yield sites that see a mix of ISEQ, proc, symbol, and ifunc block handlers: * HIR tests that a profile mixing ISEQ blocks with proc/symbol or ifunc handlers still dispatches directly on the ISEQ candidates, with the non-ISEQ handlers joining the generic fallback. * Codegen tests that symbol and ifunc handlers take the generic fallback at a polymorphic site without side exits, and that a megamorphic mix of all handler kinds returns correct results through the generic InvokeBlock. --- zjit/src/codegen_tests.rs | 63 +++++++++++++++++++ zjit/src/hir/opt_tests.rs | 129 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 56cc6b026c7c49..d5d7d04d2ee11c 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -636,6 +636,69 @@ fn test_yield_polymorphic_non_iseq_handler_falls_back() { assert_snapshot!(assert_compiles("[add_one, double, via_proc(proc { |x| x * 3 })]"), @"[11, 20, 30]"); } +#[test] +fn test_yield_polymorphic_symbol_handler_falls_back() { + // A symbol handler at a polymorphic yield site fails the ISEQ tag check and takes the + // generic InvokeBlock fallback in-line, without a side exit or another recompile. + set_call_threshold(2); + eval(" + def invoke = yield(10) + def add_one = invoke { |x| x + 1 } + def double = invoke { |x| x * 2 } + def via_sym = invoke(&:to_s) + add_one; double + add_one; double + "); + let num_profiles = get_option!(num_profiles); + for _ in 0..num_profiles + 2 { + eval("add_one; double; via_sym"); + } + assert_snapshot!(assert_compiles("[add_one, double, via_sym]"), @r#"[11, 20, "10"]"#); +} + +#[test] +fn test_yield_polymorphic_ifunc_handler_falls_back() { + // An ifunc handler (Enumerator#each yields to the enumerator's C block) at a polymorphic + // yield site fails the ISEQ tag check and takes the generic InvokeBlock fallback in-line. + // Threshold 4 keeps calls 1-3 in the profile window (num_profiles defaults to 5), so + // invoke's first compile already sees both blocks and installs the polymorphic dispatch; + // the standalone version matters here because the Enumerator calls invoke from C. + set_call_threshold(4); + eval(" + def invoke = yield(10) + def add_one = invoke { |x| x + 1 } + def double = invoke { |x| x * 2 } + def via_enum = to_enum(:invoke).to_a + add_one; double + add_one; double + "); + assert_snapshot!(assert_compiles("[add_one, double, via_enum]"), @"[11, 20, [10]]"); +} + +#[test] +fn test_yield_megamorphic_mixed_block_handlers() { + // A yield site that sees ISEQ, proc, symbol, and ifunc handlers mixed together goes + // megamorphic (each to_enum call profiles a distinct ifunc), so it compiles to the + // generic InvokeBlock and must return the right result for every handler kind. + set_call_threshold(2); + eval(" + def invoke = yield(10) + def add_one = invoke { |x| x + 1 } + def double = invoke { |x| x * 2 } + def via_proc(l) = invoke(&l) + def via_sym = invoke(&:to_s) + def via_enum = to_enum(:invoke).to_a + PR = proc { |x| x * 3 } + add_one; double + add_one; double + "); + let num_profiles = get_option!(num_profiles); + for _ in 0..num_profiles + 2 { + eval("add_one; double; via_proc(PR); via_sym; via_enum"); + } + assert_snapshot!(assert_compiles("[add_one, double, via_proc(PR), via_sym, via_enum]"), @r#"[11, 20, 30, "10", [10]]"#); +} + #[test] fn test_yield_inline_invocation_with_args() { // Plain yield with two args to a matching-arity block inlines and returns correctly. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 8cf0367a027f89..471a66d02d2867 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -4116,6 +4116,135 @@ mod hir_opt_tests { "); } + #[test] + fn test_yield_mixed_iseq_proc_symbol_profile_dispatches_on_iseqs() { + // A yield site whose profile mixes ISEQ blocks with proc and symbol handlers still + // dispatches directly on the ISEQ candidates; the non-ISEQ handlers fail the tag + // check and take the generic fallback in-line. + let result = eval(" + def invoke = yield(10) + def add_one = invoke { |x| x + 1 } + def double = invoke { |x| x * 2 } + def via_proc(l) = invoke(&l) + def via_sym = invoke(&:itself) + pr = proc { |x| x * 3 } + add_one; double; via_proc(pr); via_sym + add_one; double; via_proc(pr); via_sym + add_one; double; via_proc(pr); via_sym + add_one + double + via_proc(pr) + via_sym + "); + assert_eq!(VALUE::fixnum_from_usize(71), result); + assert_snapshot!(hir_string("invoke"), @" + fn invoke@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:Fixnum[10] = Const Value(10) + v12:CPtr = GetEP 0 + v13:CInt64 = LoadField v12, :VM_ENV_DATA_INDEX_SPECVAL@0x1000 + v15:CInt64[3] = Const CInt64(3) + v16:CInt64 = IntAnd v13, v15 + v17:CInt64[1] = Const CInt64(1) + v18:CBool = IsBitEqual v16, v17 + CondBranch v18, bb5(), bb6() + bb5(): + v20:CInt64[-4] = Const CInt64(-4) + v21:CInt64 = IntAnd v13, v20 + v22:CPtr = LoadField v21, :code_iseq@0x1001 + v23:CPtr[CPtr(0x1002)] = Const CPtr(0x1002) + v24:CBool = IsBitEqual v22, v23 + CondBranch v24, bb7(), bb8() + bb7(): + v26:BasicObject = InvokeBlockIseqDirect (0x1002), v21, v10 + Jump bb4(v26) + bb8(): + v28:CPtr[CPtr(0x1003)] = Const CPtr(0x1003) + v29:CBool = IsBitEqual v22, v28 + CondBranch v29, bb9(), bb10() + bb9(): + v31:BasicObject = InvokeBlockIseqDirect (0x1003), v21, v10 + Jump bb4(v31) + bb10(): + Jump bb6() + bb6(): + v34:BasicObject = InvokeBlock v10 # SendFallbackReason: InvokeBlock: polymorphic dispatch miss + Jump bb4(v34) + bb4(v14:BasicObject): + CheckInterrupts + Return v14 + "); + } + + #[test] + fn test_yield_mixed_iseq_ifunc_profile_dispatches_on_iseqs() { + // Like the above, but the non-ISEQ handler in the profile is an ifunc block + // (Enumerator#each yields to the enumerator's C block). The ifunc handler fails + // the ISEQ tag check and takes the generic fallback in-line. + let result = eval(" + def invoke = yield(10) + def add_one = invoke { |x| x + 1 } + def double = invoke { |x| x * 2 } + def via_enum = to_enum(:invoke).to_a + add_one; double + add_one; double + add_one; double + via_enum + add_one + double + "); + assert_eq!(VALUE::fixnum_from_usize(31), result); + assert_snapshot!(hir_string("invoke"), @" + fn invoke@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:Fixnum[10] = Const Value(10) + v12:CPtr = GetEP 0 + v13:CInt64 = LoadField v12, :VM_ENV_DATA_INDEX_SPECVAL@0x1000 + v15:CInt64[3] = Const CInt64(3) + v16:CInt64 = IntAnd v13, v15 + v17:CInt64[1] = Const CInt64(1) + v18:CBool = IsBitEqual v16, v17 + CondBranch v18, bb5(), bb6() + bb5(): + v20:CInt64[-4] = Const CInt64(-4) + v21:CInt64 = IntAnd v13, v20 + v22:CPtr = LoadField v21, :code_iseq@0x1001 + v23:CPtr[CPtr(0x1002)] = Const CPtr(0x1002) + v24:CBool = IsBitEqual v22, v23 + CondBranch v24, bb7(), bb8() + bb7(): + v26:BasicObject = InvokeBlockIseqDirect (0x1002), v21, v10 + Jump bb4(v26) + bb8(): + v28:CPtr[CPtr(0x1003)] = Const CPtr(0x1003) + v29:CBool = IsBitEqual v22, v28 + CondBranch v29, bb9(), bb10() + bb9(): + v31:BasicObject = InvokeBlockIseqDirect (0x1003), v21, v10 + Jump bb4(v31) + bb10(): + Jump bb6() + bb6(): + v34:BasicObject = InvokeBlock v10 # SendFallbackReason: InvokeBlock: polymorphic dispatch miss + Jump bb4(v34) + bb4(v14:BasicObject): + CheckInterrupts + Return v14 + "); + } + #[test] fn reload_local_across_send() { eval(" From de56b5e2f15cc867143cebc0977837bf95eba625 Mon Sep 17 00:00:00 2001 From: Shugo Maeda Date: Fri, 7 Aug 2026 13:19:09 +0900 Subject: [PATCH 9/9] Make Module#descendants follow the ancestors of the current box The subclasses lists are kept only in the prime classext and thus shared by all boxes, so a module included into a builtin class in a box was reported as a descendant from other boxes, where the include is not in the ancestors of the class. Check that the ICLASS is in the ancestors of its includer as seen from the current box. An include into a class that has no classext per box, e.g. a class defined outside the box, stays visible because it is in the ancestors from any box. Co-Authored-By: Claude Opus 5 (1M context) --- class.c | 12 ++++++++++++ test/ruby/box/descendants.rb | 25 +++++++++++++++++++++++++ test/ruby/test_box.rb | 23 +++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 test/ruby/box/descendants.rb diff --git a/class.c b/class.c index 79817af97d585f..a90a0f43de50ed 100644 --- a/class.c +++ b/class.c @@ -2142,6 +2142,17 @@ module_descendants_add(VALUE klass, struct descendants_traverse_data *data) rb_class_foreach_subclass(klass, module_descendants_recursive, (VALUE)data); } +// an include done in another box is not in the ancestors here if the includer +// has a classext per box, e.g. a module included into a builtin class +static bool +iclass_in_ancestors_p(VALUE iclass, VALUE includer) +{ + for (VALUE p = includer; p; p = RCLASS_SUPER(p)) { + if (p == iclass) return true; + } + return false; +} + static void module_descendants_recursive(VALUE entry, VALUE v) { @@ -2159,6 +2170,7 @@ module_descendants_recursive(VALUE entry, VALUE v) if (!includer || UNDEF_P(includer)) return; if (rb_objspace_garbage_object_p(includer)) return; if (RCLASS_SINGLETON_P(includer)) return; // e.g. Object#extend + if (!iclass_in_ancestors_p(entry, includer)) return; module_descendants_add(includer, data); } else { diff --git a/test/ruby/box/descendants.rb b/test/ruby/box/descendants.rb new file mode 100644 index 00000000000000..19db342e9a5269 --- /dev/null +++ b/test/ruby/box/descendants.rb @@ -0,0 +1,25 @@ +module DescendantsExt +end + +class String + include DescendantsExt +end + +class BoxedString < String +end + +MainClass = Ruby::Box.main::TestBoxDescendantsMain + +class MainClass + include DescendantsExt +end + +module Descendants + def self.ext_descendants + DescendantsExt.descendants + end + + def self.string_descendants + String.descendants + end +end diff --git a/test/ruby/test_box.rb b/test/ruby/test_box.rb index 4583db804117a8..c60fc390cef6f7 100644 --- a/test/ruby/test_box.rb +++ b/test/ruby/test_box.rb @@ -352,6 +352,29 @@ def test_methods_and_constants_added_by_include assert_equal "foo 1", @box::OpenClassWithInclude.refer_foo end + + def test_descendants_follow_ancestors_of_the_current_box + setup_box + + @box.require_relative('box/descendants') + + assert_include @box::Descendants.ext_descendants, String + assert_include @box::Descendants.string_descendants, @box::BoxedString + + # a builtin class has its own classext per box, so the include is invisible here + assert_not_include String.ancestors, @box::DescendantsExt + assert_not_include @box::DescendantsExt.descendants, String + + # a class defined outside the box shares its classext, so the include is visible here + assert_include TestBoxDescendantsMain.ancestors, @box::DescendantsExt + assert_include @box::DescendantsExt.descendants, TestBoxDescendantsMain + + assert_include @box::BoxedString.ancestors, String + assert_include String.descendants, @box::BoxedString + end +end + +class TestBoxDescendantsMain end module ProcLookupTestA