From bb472c530a91e4b26a76d24c6e607cf38d0a58f0 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Mon, 22 Jun 2026 13:11:11 +0100 Subject: [PATCH 01/39] [ruby/ipaddr] extra multicast/unicast predicates This adds the following predicate methods to IPAddr objects: * `#multicast?`: whether the ip address is multicast * `#link_local_multicast?`: whether the ip address is a link-local multicast it also alias `IPAddr#link_local?` as `IPAddr#link_local_unicast?`, which is more accurate (whether the original should be deprecated is probably a separate discussion). https://github.com/ruby/ipaddr/commit/332ae1ffbf --- lib/ipaddr.rb | 40 +++++++++++++++++++++++++++++++++++++++- test/test_ipaddr.rb | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/lib/ipaddr.rb b/lib/ipaddr.rb index 70b804f642f099..a71772e32b05ad 100644 --- a/lib/ipaddr.rb +++ b/lib/ipaddr.rb @@ -328,7 +328,7 @@ def private? end end - # Returns true if the ipaddr is a link-local address. IPv4 + # Returns true if the ipaddr is a link-local unicast address. IPv4 # addresses in 169.254.0.0/16 reserved by RFC 3927 and link-local # IPv6 Unicast Addresses in fe80::/10 reserved by RFC 4291 are # considered link-local. Link-local IPv4 addresses in the @@ -347,6 +347,44 @@ def link_local? end end + alias link_local_unicast? link_local? + + # Returns true if the ipaddr is a multicast address. IPv4 + # addresses in 224.0.0.0/4 and IPv6 multicast addresses + # in ff00::/8 are considered multicast. Multicast IPv4 addresses in the + # IPv4-mapped IPv6 address range are also considered multicast. + def multicast? + case @family + when Socket::AF_INET + @addr & 0xf0000000 == 0xe0000000 # 224.0.0.0/4 + when Socket::AF_INET6 + @addr & 0xff00_0000_0000_0000_0000_0000_0000_0000 == 0xff00_0000_0000_0000_0000_0000_0000_0000 || # ff00::/8 + (@addr >> 32 == 0xffff && + @addr & 0xf0000000 == 0xe0000000 # ::ffff:224.0.0.0/4 + ) + else + raise AddressFamilyError, "unsupported address family" + end + end + + # Returns true if the ipaddr is a link-local multicast address. IPv4 + # addresses in 224.0.0.0/24 and link-local IPv6 Multicast Addresses in ff02::/16 + # are considered link-local multicast. Link-local multicast IPv4 addresses in the + # IPv4-mapped IPv6 address range are also considered link-local multicast. + def link_local_multicast? + case @family + when Socket::AF_INET + @addr & 0xffffff00 == 0xe0000000 # 224.0.0.0/24 Local Network Control Block + when Socket::AF_INET6 + @addr & 0xffff_0000_0000_0000_0000_0000_0000_0000 == 0xff02_0000_0000_0000_0000_0000_0000_0000 || # ff02::/16 + (@addr >> 32 == 0xffff && ( + @addr & 0xffff0000 == 0xe0000000 # ::ffff:224.0.0.0/24 + )) + else + raise AddressFamilyError, "unsupported address family" + end + end + # Returns true if the ipaddr is an IPv4-mapped IPv6 address. def ipv4_mapped? return ipv6? && (@addr >> 32) == 0xffff diff --git a/test/test_ipaddr.rb b/test/test_ipaddr.rb index 9725ab31c173dc..fe940204e4207b 100644 --- a/test/test_ipaddr.rb +++ b/test/test_ipaddr.rb @@ -623,6 +623,51 @@ def test_link_local? assert_equal(false, IPAddr.new('2001:db8:1:1:0:ffff:a9fe:101').link_local?) end + def test_multicast? + assert_equal(false, IPAddr.new('192.168.0.0').multicast?) + assert_equal(false, IPAddr.new('169.254.1.1').multicast?) + assert_equal(false, IPAddr.new('169.254.254.255').multicast?) + + # notable ipv4 multicast addresses + assert_equal(true, IPAddr.new('224.0.0.0').multicast?) + assert_equal(true, IPAddr.new('224.0.0.1').multicast?) + assert_equal(true, IPAddr.new('224.0.0.6').multicast?) + assert_equal(true, IPAddr.new('224.0.1.41').multicast?) + assert_equal(true, IPAddr.new('224.0.1.129').multicast?) + assert_equal(true, IPAddr.new('224.0.23.12').multicast?) + assert_equal(true, IPAddr.new('239.255.255.250').multicast?) + assert_equal(true, IPAddr.new('239.255.255.253').multicast?) + + assert_equal(false, IPAddr.new('::1').multicast?) + assert_equal(false, IPAddr.new('::').multicast?) + assert_equal(false, IPAddr.new('fb84:8bf7:e905::1').multicast?) + + # notable ipv6 multicast addresses + assert_equal(true, IPAddr.new('ff02::1').multicast?) + assert_equal(true, IPAddr.new('ff02::2').multicast?) + assert_equal(true, IPAddr.new('ff02::5').multicast?) + assert_equal(true, IPAddr.new('ff02::6').multicast?) + assert_equal(true, IPAddr.new('ff02::8').multicast?) + assert_equal(true, IPAddr.new('ff02::1:2').multicast?) + assert_equal(true, IPAddr.new('ff02::1:3').multicast?) + assert_equal(true, IPAddr.new('ff05::101').multicast?) + end + + def test_link_local_multicast? + assert_equal(false, IPAddr.new('192.168.0.0').link_local_multicast?) + assert_equal(false, IPAddr.new('169.254.1.1').link_local_multicast?) + assert_equal(false, IPAddr.new('169.254.254.255').link_local_multicast?) + assert_equal(false, IPAddr.new('239.0.0.0').link_local_multicast?) + + assert_equal(true, IPAddr.new('224.0.0.0').link_local_multicast?) + + assert_equal(false, IPAddr.new('::1').link_local_multicast?) + assert_equal(false, IPAddr.new('::').link_local_multicast?) + assert_equal(false, IPAddr.new('ff05::1').link_local_multicast?) + assert_equal(true, IPAddr.new('ff02::1').link_local_multicast?) + assert_equal(true, IPAddr.new('ff02::2').link_local_multicast?) + end + def test_hash a1 = IPAddr.new('192.168.2.0') a2 = IPAddr.new('192.168.2.0') From e9444aa0dadf59777d07509e23006e741ff66838 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 10 Jun 2026 09:26:25 +0900 Subject: [PATCH 02/39] [Feature #22185] Add alignment directives to pack templates --- NEWS.md | 6 ++ doc/language/packed_data.md | 26 +++++ pack.c | 131 +++++++++++++++++++++++-- spec/ruby/core/array/pack/x_spec.rb | 16 +++ spec/ruby/core/string/unpack/x_spec.rb | 14 +++ test/ruby/test_pack.rb | 15 +++ 6 files changed, 199 insertions(+), 9 deletions(-) diff --git a/NEWS.md b/NEWS.md index 26d5157c4aa535..6392e1ba479187 100644 --- a/NEWS.md +++ b/NEWS.md @@ -29,6 +29,9 @@ Note: We're only listing outstanding class updates. * `Array#pack` accepts new formats `R` and `r` for unsigned and signed LEB128 encoded integers. [[Feature #21785]] + * `Array#pack` accepts new formats `x!` and `@!` to align the current + offset to a byte boundary or to the ABI alignment of another + directive. [[Feature #22185]] * ENV @@ -79,6 +82,9 @@ Note: We're only listing outstanding class updates. * `String#unpack` and `String#unpack1` accept a new format `^` that returns the current offset. Useful when combined with variable width formats like LEB128. [[Feature #21796]] + * `String#unpack` and `String#unpack1` accept new formats `x!` and + `@!` to align the current offset to a byte boundary or to the ABI + alignment of another directive. [[Feature #22185]] * Symbol diff --git a/doc/language/packed_data.md b/doc/language/packed_data.md index 1b133367d6845d..ce4b85ba0c425f 100644 --- a/doc/language/packed_data.md +++ b/doc/language/packed_data.md @@ -879,6 +879,32 @@ for one byte in the input or output string. "\x00\x00\x02".unpack("x4C") # Raises ArgumentError ``` +- `'x!'` - Align to the given byte boundary or directive alignment; + for packing, null fill if necessary. With `buffer:` or `offset:`, + alignment is relative to the starting position: + + ```ruby + [1, 2].pack("C x!4 C") # => "\x01\x00\x00\x00\x02" + [1, 2].pack("C x!i i") # Aligns to the native int alignment. + ``` + + For unpacking, skip forward to the aligned offset: + + ```ruby + "\x01\x00\x00\x00\x02".unpack("C x!4 C") # => [1, 2] + ``` + +- `'@!'` - Align to the given byte boundary or directive alignment + from the beginning of the output or input string: + + ```ruby + buffer = +"z" + [1, 2].pack("C @!4 C", buffer: buffer) + buffer # => "z\x01\x00\x00\x02" + + "z\x01\x00\x00\x02".unpack("C @!4 C", offset: 1) # => [1, 2] + ``` + - `'^'` - Only for unpacking; the current position: ```ruby diff --git a/pack.c b/pack.c index 56af4ea099cae6..4b6595b45a9e45 100644 --- a/pack.c +++ b/pack.c @@ -72,8 +72,12 @@ is_bigendian(void) #ifdef NATINT_PACK # define NATINT_LEN(type,len) (natint?(int)sizeof(type):(int)(len)) +# define NATINT_ALIGN(type,len) (natint?(int)RUBY_ALIGNOF(type):(int)(len)) +# define USING_NATINT(expr) expr #else # define NATINT_LEN(type,len) ((int)sizeof(type)) +# define NATINT_ALIGN(type,len) ((int)RUBY_ALIGNOF(type)) +# define USING_NATINT(expr) /* void */ #endif typedef union { @@ -218,9 +222,7 @@ pack_modifiers(const char *p, const char *pend, char type, int *natint, int *exp case '_': case '!': if (strchr(natstr, type)) { -#ifdef NATINT_PACK - *natint = 1; -#endif + USING_NATINT(*natint = 1); p++; } else { @@ -245,6 +247,91 @@ pack_modifiers(const char *p, const char *pend, char type, int *natint, int *exp return p; } +#ifndef NATINT_PACK +# define pack_alignof(t, n) pack_alignof(t) +#endif +static size_t +pack_alignof(char type, int natint) +{ + switch (type) { + case 'c': case 'C': + return RUBY_ALIGNOF(char); + case 's': case 'S': + return NATINT_ALIGN(short, 2); + case 'i': case 'I': + return RUBY_ALIGNOF(int); + case 'l': case 'L': + return NATINT_ALIGN(long, 4); + case 'q': case 'Q': + return RUBY_ALIGNOF(int64_t); + case 'j': + return RUBY_ALIGNOF(intptr_t); + case 'J': + return RUBY_ALIGNOF(uintptr_t); + case 'n': case 'v': + return RUBY_ALIGNOF(uint16_t); + case 'N': case 'V': + return RUBY_ALIGNOF(uint32_t); + case 'f': case 'F': case 'e': case 'g': + return RUBY_ALIGNOF(float); + case 'd': case 'D': case 'E': case 'G': + return RUBY_ALIGNOF(double); + case 'p': case 'P': + return RUBY_ALIGNOF(char *); + default: + return 0; + } +} + +static long +pack_align_pad(long pos, long base, size_t alignment) +{ + long offset, mod; + + if (alignment <= 1) return 0; + if (alignment > LONG_MAX) rb_raise(rb_eRangeError, "alignment too big"); + offset = pos - base; + mod = offset % (long)alignment; + if (mod < 0) mod += alignment; + return mod ? (long)alignment - mod : 0; +} + +static char * +pack_alignment(const char *p, const char *pend, VALUE fmt, size_t *alignment) +{ + char type; + int explicit_endian = 0; + USING_NATINT(int natint = 0); + + if (p >= pend) { + rb_raise(rb_eArgError, "missing alignment"); + } + type = *p++; + p = pack_modifiers(p, pend, type, &natint, &explicit_endian); + if (explicit_endian) { + rb_raise(rb_eArgError, "endian modifier is not allowed for alignment"); + } + *alignment = pack_alignof(type, natint); + if (!*alignment) { + unknown_directive("alignment", type, fmt); + } + return (char *)p; +} + +static char * +pack_alignment_size(const char *p, const char *pend, VALUE fmt, size_t *alignment) +{ + if (p < pend && ISDIGIT(*p)) { + errno = 0; + *alignment = STRTOUL(p, (char**)&p, 10); + if (*alignment <= 0 || errno) { + rb_raise(rb_eRangeError, "invalid alignment"); + } + return (char *)p; + } + return pack_alignment(p, pend, fmt, alignment); +} + static VALUE pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer) { @@ -254,6 +341,7 @@ pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer) const char *ptr; int enc_info = 1; /* 0 - BINARY, 1 - US-ASCII, 2 - UTF-8 */ int integer_size, bigendian_p; + long align_base; StringValue(fmt); rb_must_asciicompat(fmt); @@ -271,6 +359,7 @@ pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer) } idx = 0; + align_base = RSTRING_LEN(res); #define TOO_FEW (rb_raise(rb_eArgError, toofew), 0) #define MORE_ITEM (idx < RARRAY_LEN(ary)) @@ -281,15 +370,25 @@ pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer) while (p < pend) { int explicit_endian = 0; + size_t align = 0; if (RSTRING_END(fmt) != pend) { rb_raise(rb_eRuntimeError, "format string modified"); } const char type = *p++; /* get data type */ -#ifdef NATINT_PACK - int natint = 0; /* native integer */ -#endif + USING_NATINT(int natint = 0); /* native integer */ if (skip_blank(p, type)) continue; + + /* Directives that do not take modifiers. */ + if ((type == 'x' || type == '@') && p < pend && *p == '!') { + p++; + p = pack_alignment_size(p, pend, fmt, &align); + len = pack_align_pad(RSTRING_LEN(res), type == '@' ? 0 : align_base, align); + rb_str_modify_expand(res, len); + str_expand_fill(res, '\0', len); + continue; + } + p = pack_modifiers(p, pend, type, &natint, &explicit_endian); if (*p == '*') { /* set data length */ @@ -1005,6 +1104,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, VALUE ofs, enum unpack_mode mode) long len; AVOID_CC_BUG long tmp_len; int signed_p, integer_size, bigendian_p; + long align_base; #define UNPACK_PUSH(item) do {\ VALUE item_val = (item);\ if ((mode) == UNPACK_BLOCK) {\ @@ -1031,6 +1131,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, VALUE ofs, enum unpack_mode mode) s = RSTRING_PTR(str); send = s + len; s += offset; + align_base = offset; p = RSTRING_PTR(fmt); pend = p + RSTRING_LEN(fmt); @@ -1041,12 +1142,24 @@ pack_unpack_internal(VALUE str, VALUE fmt, VALUE ofs, enum unpack_mode mode) while (p < pend) { int explicit_endian = 0; const char type = *p++; -#ifdef NATINT_PACK - int natint = 0; /* native integer */ -#endif + size_t align = 0; int star = 0; + USING_NATINT(int natint = 0); /* native integer */ if (skip_blank(p, type)) continue; + + /* Directives that do not take modifiers. */ + if ((type == 'x' || type == '@') && p < pend && *p == '!') { + p++; + p = pack_alignment_size(p, pend, fmt, &align); + len = pack_align_pad(s - RSTRING_PTR(str), type == '@' ? 0 : align_base, align); + if (len > send - s) { + rb_raise(rb_eArgError, type == '@' ? "@ outside of string" : "x outside of string"); + } + s += len; + continue; + } + p = pack_modifiers(p, pend, type, &natint, &explicit_endian); if (p >= pend) diff --git a/spec/ruby/core/array/pack/x_spec.rb b/spec/ruby/core/array/pack/x_spec.rb index 7ff587a01edd8d..1d26de6c51e11c 100644 --- a/spec/ruby/core/array/pack/x_spec.rb +++ b/spec/ruby/core/array/pack/x_spec.rb @@ -32,6 +32,22 @@ [].pack("x*").should == "" [1, 2].pack("Cx*C").should == "\x01\x02" end + + ruby_version_is "4.1" do + it "aligns to the given byte boundary with the '!' modifier" do + [1, 2].pack("C x!4 C").should == "\x01\x00\x00\x00\x02" + end + + it "aligns from the beginning of the output with '@!'" do + buffer = +"z" + [1, 2].pack("C @!4 C", buffer: buffer) + buffer.should == "z\x01\x00\x00\x02" + end + + it "aligns to a directive's alignment with the '!' modifier" do + [1, 2].pack("C x!i i").should == [1, 2].pack("i< i") + end + end end describe "Array#pack with format 'X'" do diff --git a/spec/ruby/core/string/unpack/x_spec.rb b/spec/ruby/core/string/unpack/x_spec.rb index fb2e79fc1f765a..a93a4dba995d50 100644 --- a/spec/ruby/core/string/unpack/x_spec.rb +++ b/spec/ruby/core/string/unpack/x_spec.rb @@ -59,4 +59,18 @@ it "raises an ArgumentError if the count exceeds the size of the String" do -> { "\x01\x02\x03\x04".unpack("C2x3C") }.should.raise(ArgumentError) end + + ruby_version_is "4.1" do + it "aligns to the given byte boundary with the '!' modifier" do + "\x01\x00\x00\x00\x02".unpack("C x!4 C").should == [1, 2] + end + + it "aligns from the beginning of the String with '@!'" do + "z\x01\x00\x00\x02".unpack("C @!4 C", offset: 1).should == [1, 2] + end + + it "aligns to a directive's alignment with the '!' modifier" do + [1, 2].pack("i< i").unpack("C x!i i").should == [1, 2] + end + end end diff --git a/test/ruby/test_pack.rb b/test/ruby/test_pack.rb index 6e5f0fe7ff948f..e1dd3968378204 100644 --- a/test/ruby/test_pack.rb +++ b/test/ruby/test_pack.rb @@ -939,6 +939,21 @@ def test_unpack_offset assert_equal [nil], "a".unpack("C", offset: 1) end + def test_pack_alignment + assert_equal "\x01\x00\x00\x00\x02".b, [1, 2].pack("C x!4 C") + assert_equal [1, 2], "\x01\x00\x00\x00\x02".b.unpack("C x!4 C") + + buffer = +"z" + [1, 2].pack("C x!4 C", buffer: buffer) + assert_equal "z\x01\x00\x00\x00\x02".b, buffer + assert_equal [1, 2], buffer.unpack("C x!4 C", offset: 1) + + buffer = +"z" + [1, 2].pack("C @!4 C", buffer: buffer) + assert_equal "z\x01\x00\x00\x02".b, buffer + assert_equal [1, 2], buffer.unpack("C @!4 C", offset: 1) + end + def test_monkey_pack assert_separately([], <<-'end;') $-w = false From 86951165b81bed248b9adab56c2f0141025aa06b Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 15 Jul 2026 10:57:54 +0200 Subject: [PATCH 03/39] Get rid of ROBJECT_HEAP flag This flag is now entirely redundant with the shape_id layout bits. --- debug.c | 1 - ext/objspace/objspace_dump.c | 2 +- gc.c | 44 ++++---- include/ruby/internal/core/robject.h | 29 ----- internal/imemo.h | 37 ------- internal/object.h | 23 ---- object.c | 7 +- shape.c | 15 ++- shape.h | 158 ++++++++++++++++++++------- variable.c | 5 +- yjit.c | 6 + yjit/bindgen/src/main.rs | 1 + yjit/src/cruby.rs | 4 +- yjit/src/cruby_bindings.inc.rs | 3 +- zjit/src/cruby.rs | 6 +- zjit/src/cruby_bindings.inc.rs | 2 - zjit/src/hir/opt_tests.rs | 6 +- zjit/src/profile.rs | 2 +- 18 files changed, 169 insertions(+), 182 deletions(-) diff --git a/debug.c b/debug.c index 730f860e7af988..0ced0df9c7341c 100644 --- a/debug.c +++ b/debug.c @@ -52,7 +52,6 @@ const union { enum ruby_econv_flag_type econv_flag_types; rb_econv_result_t econv_result; enum ruby_preserved_encindex encoding_index; - enum ruby_robject_flags robject_flags; enum ruby_rmodule_flags rmodule_flags; enum ruby_rstring_flags rstring_flags; enum ruby_rarray_flags rarray_flags; diff --git a/ext/objspace/objspace_dump.c b/ext/objspace/objspace_dump.c index 68ada01af3e514..fb25c400bd4ac3 100644 --- a/ext/objspace/objspace_dump.c +++ b/ext/objspace/objspace_dump.c @@ -592,7 +592,7 @@ dump_object(VALUE obj, struct dump_config *dc) break; case T_OBJECT: - if (!FL_TEST_RAW(obj, ROBJECT_HEAP)) { + if (rb_obj_shape_embedded_p(obj)) { dump_append(dc, ", \"embedded\":true"); } diff --git a/gc.c b/gc.c index ef193dc8efacf3..c41e7867fba688 100644 --- a/gc.c +++ b/gc.c @@ -1434,7 +1434,6 @@ rb_gc_obj_needs_cleanup_p(VALUE obj) switch (flags & RUBY_T_MASK) { case T_OBJECT: - if (flags & ROBJECT_HEAP) return true; return false; case T_DATA: @@ -3300,20 +3299,20 @@ rb_gc_mark_children(void *objspace, VALUE obj) } case T_OBJECT: { - uint32_t len; - if (FL_TEST_RAW(obj, ROBJECT_HEAP)) { - if (!rb_gc_checking_shareable()) { - gc_mark_internal(ROBJECT(obj)->as.extended); - } - } - else { + shape_id_t shape_id = RBASIC_SHAPE_ID(obj); + if (rb_shape_embedded_p(shape_id)) { + uint32_t len = RSHAPE_LEN(shape_id); const VALUE * const ptr = ROBJECT(obj)->as.ary; - len = ROBJECT_FIELDS_COUNT_NOT_COMPLEX(obj); for (uint32_t i = 0; i < len; i++) { gc_mark_internal(ptr[i]); } } + else { + if (!rb_gc_checking_shareable()) { + gc_mark_internal(ROBJECT(obj)->as.extended); + } + } break; } @@ -3680,15 +3679,15 @@ gc_ref_update_object(void *objspace, VALUE v) RUBY_ASSERT(rb_gc_obj_slot_size(v) == rb_obj_shape_slot_size(v)); shape_id_t shape_id = RBASIC_SHAPE_ID(v); - if (FL_TEST_RAW(v, ROBJECT_HEAP)) { + if (!rb_shape_embedded_p(shape_id)) { UPDATE_IF_MOVED(objspace, ROBJECT(v)->as.extended); if (!rb_shape_complex_p(shape_id) && rb_shape_embedded_capacity(shape_id) >= RSHAPE_LEN(shape_id)) { VALUE *embedded_fields = ROBJECT_EMBEDDED_FIELDS(v); VALUE *extended_fields = ROBJECT_FIELDS(v); MEMCPY(embedded_fields, extended_fields, VALUE, RSHAPE_LEN(shape_id)); - FL_UNSET_RAW(v, ROBJECT_HEAP); - RBASIC_SET_FULL_SHAPE_ID(v, rb_shape_transition_robject(shape_id)); + shape_id = rb_shape_transition_robject(shape_id); + RBASIC_SET_FULL_SHAPE_ID(v, shape_id); rb_gc_writebarrier_remember(v); } else { @@ -3697,7 +3696,8 @@ gc_ref_update_object(void *objspace, VALUE v) } VALUE *ptr = ROBJECT_FIELDS(v); - for (uint32_t i = 0; i < ROBJECT_FIELDS_COUNT(v); i++) { + attr_index_t len = RSHAPE_LEN(shape_id); + for (attr_index_t i = 0; i < len; i++) { UPDATE_IF_MOVED(objspace, ptr[i]); } } @@ -4855,18 +4855,20 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU } case T_OBJECT: { - if (FL_TEST_RAW(obj, ROBJECT_HEAP)) { - if (rb_obj_shape_complex_p(obj)) { - size_t hash_len = rb_st_table_size(ROBJECT_FIELDS_HASH(obj)); - APPEND_F("(complex) len:%zu", hash_len); + shape_id_t shape_id = RBASIC_SHAPE_ID(obj); + if (rb_shape_embedded_p(shape_id)) { + APPEND_F("(embed) len:%d capa:%d", RSHAPE_LEN(shape_id), RSHAPE_CAPACITY(shape_id)); + } + else { + VALUE fields_obj = ROBJECT_FIELDS_OBJ(obj); + if (rb_shape_complex_p(shape_id)) { + size_t hash_len = rb_st_table_size(rb_imemo_fields_complex_tbl(fields_obj)); + APPEND_F("(complex) len:%zu extended:%p", hash_len, (void *)fields_obj); } else { - APPEND_F("len:%d capa:%d extended:%p", RSHAPE_LEN(RBASIC_SHAPE_ID(obj)), ROBJECT_FIELDS_CAPACITY(obj), (void *)ROBJECT_FIELDS_OBJ(obj)); + APPEND_F("(extended) len:%d capa:%d extended:%p", RSHAPE_LEN(shape_id), RSHAPE_CAPACITY(shape_id), (void *)fields_obj); } } - else { - APPEND_F("(embed) len:%d capa:%d", RSHAPE_LEN(RBASIC_SHAPE_ID(obj)), ROBJECT_FIELDS_CAPACITY(obj)); - } } break; case T_DATA: { diff --git a/include/ruby/internal/core/robject.h b/include/ruby/internal/core/robject.h index c8917c0f8e2b19..c4d3c0d5cf3429 100644 --- a/include/ruby/internal/core/robject.h +++ b/include/ruby/internal/core/robject.h @@ -43,39 +43,10 @@ #define ROBJECT(obj) RBIMPL_CAST((struct RObject *)(obj)) /** @cond INTERNAL_MACRO */ #define ROBJECT_EMBED_LEN_MAX ROBJECT_EMBED_LEN_MAX -#define ROBJECT_HEAP ROBJECT_HEAP #define ROBJECT_FIELDS_CAPACITY ROBJECT_FIELDS_CAPACITY #define ROBJECT_FIELDS ROBJECT_FIELDS /** @endcond */ -/** - * @private - * - * Bits that you can set to ::RBasic::flags. - */ -enum ruby_robject_flags { - /** - * This flag marks that the object's instance variables are stored in an - * external heap buffer. - * Normally, instance variable references are stored inside the object slot, - * but if it overflow, Ruby may have to allocate a separate buffer and spills - * the instance variables there. - * This flag denotes that situation. - * - * @warning This bit has to be considered read-only. Setting/clearing - * this bit without corresponding fix up must cause immediate - * SEGV. Also, internal structures of an object change - * dynamically and transparently throughout of its lifetime. - * Don't assume it being persistent. - * - * @internal - * - * 3rd parties must not be aware that there even is more than one way to - * store instance variables. Might better be hidden. - */ - ROBJECT_HEAP = RUBY_FL_USER4 -}; - struct st_table; /** diff --git a/internal/imemo.h b/internal/imemo.h index 0bd9fc0bfb50d0..da8196f47167c7 100644 --- a/internal/imemo.h +++ b/internal/imemo.h @@ -252,8 +252,6 @@ struct rb_fields { // IMEMO/fields and T_OBJECT have exactly the same layout. // This is useful for JIT and common codepaths. -#define OBJ_FIELD_HEAP ROBJECT_HEAP -STATIC_ASSERT(imemo_fields_flags, OBJ_FIELD_HEAP == IMEMO_FL_USER0); STATIC_ASSERT(imemo_fields_embed_offset, offsetof(struct RObject, as.ary) == offsetof(struct rb_fields, as.embed.fields)); #define IMEMO_OBJ_FIELDS(fields) ((struct rb_fields *)fields) @@ -292,39 +290,4 @@ rb_imemo_fields_owner(VALUE fields_obj) return CLASS_OF(fields_obj); } -static inline VALUE * -rb_imemo_fields_ptr(VALUE fields_obj) -{ - if (!fields_obj) { - return NULL; - } - - RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields) || RB_TYPE_P(fields_obj, T_OBJECT)); - - if (UNLIKELY(FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP))) { - RUBY_ASSERT(RB_TYPE_P(fields_obj, T_OBJECT)); - fields_obj = ROBJECT(fields_obj)->as.extended; - RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); - } - - return IMEMO_OBJ_FIELDS(fields_obj)->as.embed.fields; -} - -static inline st_table * -rb_imemo_fields_complex_tbl(VALUE fields_obj) -{ - if (!fields_obj) { - return NULL; - } - - RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); - RUBY_ASSERT(!FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP)); - - // Some codepaths unconditionally access the fields_ptr, and assume it can be used as st_table if the - // shape is complex. - RUBY_ASSERT((st_table *)rb_imemo_fields_ptr(fields_obj) == &IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table); - - return &IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table; -} - #endif /* INTERNAL_IMEMO_H */ diff --git a/internal/object.h b/internal/object.h index 2c531186ba3d75..3574f2d89f439c 100644 --- a/internal/object.h +++ b/internal/object.h @@ -68,35 +68,12 @@ RBASIC_SET_CLASS(VALUE obj, VALUE klass) RB_OBJ_WRITTEN(obj, oldv, klass); } -static inline VALUE -ROBJECT_FIELDS_OBJ(VALUE obj) -{ - RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); - - return FL_TEST_RAW(obj, ROBJECT_HEAP) ? ROBJECT(obj)->as.extended : obj; -} - -static inline VALUE * -ROBJECT_EMBEDDED_FIELDS(VALUE obj) -{ - return ROBJECT(obj)->as.ary; -} - -static inline VALUE * -ROBJECT_FIELDS(VALUE obj) -{ - RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); - - return ROBJECT_EMBEDDED_FIELDS(ROBJECT_FIELDS_OBJ(obj)); -} - static inline void ROBJECT_SET_EXTENDED(VALUE obj, VALUE fields_obj) { RUBY_ASSERT(RB_TYPE_P(obj, T_OBJECT)); RUBY_ASSERT(RB_TYPE_P(fields_obj, T_IMEMO)); RB_OBJ_WRITE(obj, &ROBJECT(obj)->as.extended, fields_obj); - FL_SET_RAW(obj, ROBJECT_HEAP); } static inline size_t diff --git a/object.c b/object.c index 033b3dc35fb5de..2eb958fb151417 100644 --- a/object.c +++ b/object.c @@ -46,9 +46,6 @@ /* Flags of RObject * - * 4: ROBJECT_HEAP - * The object has its instance variables in a separately allocated buffer. - * This can be either a flat buffer of reference, or an st_table for complex objects. */ /*! @@ -355,8 +352,8 @@ rb_obj_copy_ivar(VALUE dest, VALUE obj) RUBY_ASSERT(src_num_ivs <= dest_capa); if (initial_capa < dest_capa) { // We we need to transition the object to an extended layout. - VALUE fields_obj = rb_imemo_fields_new(dest, dest_shape_id, false); - ROBJECT_SET_EXTENDED(dest, fields_obj); + rb_obj_replace_fields(dest, rb_imemo_fields_new(dest, dest_shape_id, false)); + dest_buf = ROBJECT_FIELDS(dest); rb_shape_copy_fields(dest, dest_buf, dest_shape_id, src_buf, src_shape_id); RBASIC_SET_SHAPE_ID_WITH_LAYOUT(dest, dest_shape_id, SHAPE_ID_LAYOUT_EXTENDED); diff --git a/shape.c b/shape.c index c54c86b937c923..5c59d2aee22aca 100644 --- a/shape.c +++ b/shape.c @@ -1216,8 +1216,9 @@ static shape_id_t rb_shape_expected_layout(VALUE obj) { switch (BUILTIN_TYPE(obj)) { - case T_OBJECT: - return FL_TEST_RAW(obj, ROBJECT_HEAP) ? SHAPE_ID_LAYOUT_EXTENDED : SHAPE_ID_LAYOUT_ROBJECT; + case T_OBJECT: { + return SHAPE_ID_LAYOUT_ROBJECT; + } case T_CLASS: case T_MODULE: if (FL_TEST_RAW(obj, RCLASS_BOXABLE)) { @@ -1278,8 +1279,10 @@ rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) shape_id_t actual_layout = rb_shape_layout(rb_obj_shape_id(obj)); shape_id_t expected_layout = rb_shape_expected_layout(obj); if (actual_layout != expected_layout) { - rb_bug("shape_id layout mismatch: expected=%s actual=%s shape_id=%u obj=%s", - shape_layout_name(expected_layout), shape_layout_name(actual_layout), shape_id, rb_obj_info(obj)); + if (!(RB_TYPE_P(obj, T_OBJECT) && actual_layout == SHAPE_ID_LAYOUT_EXTENDED)) { + rb_bug("shape_id layout mismatch: expected=%s actual=%s shape_id=%u obj=%s", + shape_layout_name(expected_layout), shape_layout_name(actual_layout), shape_id, rb_obj_info(obj)); + } } if (shape_id == ROOT_SHAPE_ID) { @@ -1294,10 +1297,10 @@ rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) // Ensure complex object don't appear as embedded if (RB_TYPE_P(obj, T_OBJECT)) { - RUBY_ASSERT(FL_TEST_RAW(obj, ROBJECT_HEAP)); + RUBY_ASSERT(rb_obj_shape_extended_p(obj)); } else if (IMEMO_TYPE_P(obj, imemo_fields)) { - RUBY_ASSERT(!FL_TEST_RAW(obj, ROBJECT_HEAP)); + RUBY_ASSERT(rb_obj_shape_embedded_p(obj)); } } else { diff --git a/shape.h b/shape.h index 567d488255e4b6..e4423f9327370c 100644 --- a/shape.h +++ b/shape.h @@ -191,6 +191,30 @@ rb_shape_layout(shape_id_t shape_id) return shape_id & SHAPE_ID_LAYOUT_MASK; } +static inline bool +rb_shape_embedded_p(shape_id_t shape_id) +{ + return rb_shape_layout(shape_id) == SHAPE_ID_LAYOUT_ROBJECT; +} + +static inline bool +rb_shape_extended_p(shape_id_t shape_id) +{ + return rb_shape_layout(shape_id) == SHAPE_ID_LAYOUT_EXTENDED; +} + +static inline bool +rb_obj_shape_embedded_p(VALUE obj) +{ + return rb_shape_embedded_p(RBASIC_SHAPE_ID(obj)); +} + +static inline bool +rb_obj_shape_extended_p(VALUE obj) +{ + return rb_shape_extended_p(RBASIC_SHAPE_ID(obj)); +} + // Assigns the entire shape_id. // shape_id_t is composed of two parts: // - The layout and capacity part, which never changes except on GC compaction. @@ -381,49 +405,20 @@ RSHAPE_EDGE_NAME(shape_id_t shape_id) return RSHAPE(shape_id)->edge_name; } -static inline uint32_t -ROBJECT_FIELDS_CAPACITY(VALUE obj) +static inline VALUE * +rb_imemo_fields_ptr(VALUE fields_obj) { - RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); - // Asking for capacity doesn't make sense when the object is using - // a hash table for storing instance variables - RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - return RSHAPE_CAPACITY(RBASIC_SHAPE_ID(obj)); -} - -static inline st_table * -ROBJECT_FIELDS_HASH(VALUE obj) -{ - RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); - RUBY_ASSERT(rb_obj_shape_complex_p(obj)); - RUBY_ASSERT(FL_TEST_RAW(obj, ROBJECT_HEAP)); - - return rb_imemo_fields_complex_tbl(ROBJECT(obj)->as.extended); -} - -static inline uint32_t -ROBJECT_FIELDS_COUNT_COMPLEX(VALUE obj) -{ - return (uint32_t)rb_st_table_size(ROBJECT_FIELDS_HASH(obj)); -} - -static inline uint32_t -ROBJECT_FIELDS_COUNT_NOT_COMPLEX(VALUE obj) -{ - RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); - RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - return RSHAPE(RBASIC_SHAPE_ID(obj))->next_field_index; -} - -static inline uint32_t -ROBJECT_FIELDS_COUNT(VALUE obj) -{ - if (rb_obj_shape_complex_p(obj)) { - return ROBJECT_FIELDS_COUNT_COMPLEX(obj); + if (!fields_obj) { + return NULL; } - else { - return ROBJECT_FIELDS_COUNT_NOT_COMPLEX(obj); + + if (UNLIKELY(rb_obj_shape_extended_p(fields_obj))) { + RUBY_ASSERT(RB_TYPE_P(fields_obj, T_OBJECT)); + fields_obj = ROBJECT(fields_obj)->as.extended; + RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); } + + return IMEMO_OBJ_FIELDS(fields_obj)->as.embed.fields; } static inline uint32_t @@ -688,4 +683,87 @@ rb_setivar_cache_revalidate(shape_id_t shape_id, shape_id_t fields_shape_id, rb_ return rb_shape_transition_offset(shape_id, cache.dest_shape_offset); } +static inline st_table * +rb_imemo_fields_complex_tbl(VALUE fields_obj) +{ + if (!fields_obj) { + return NULL; + } + + RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); + + // Some codepaths unconditionally access the fields_ptr, and assume it can be used as st_table if the + // shape is complex. + RUBY_ASSERT((st_table *)rb_imemo_fields_ptr(fields_obj) == &IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table); + + return &IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table; +} + +static inline uint32_t +ROBJECT_FIELDS_CAPACITY(VALUE obj) +{ + RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); + // Asking for capacity doesn't make sense when the object is using + // a hash table for storing instance variables + RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); + return RSHAPE_CAPACITY(RBASIC_SHAPE_ID(obj)); +} + +static inline st_table * +ROBJECT_FIELDS_HASH(VALUE obj) +{ + RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); + RUBY_ASSERT(rb_obj_shape_complex_p(obj)); + RUBY_ASSERT(rb_obj_shape_extended_p(obj)); + + return rb_imemo_fields_complex_tbl(ROBJECT(obj)->as.extended); +} + +static inline uint32_t +ROBJECT_FIELDS_COUNT_COMPLEX(VALUE obj) +{ + return (uint32_t)rb_st_table_size(ROBJECT_FIELDS_HASH(obj)); +} + +static inline uint32_t +ROBJECT_FIELDS_COUNT_NOT_COMPLEX(VALUE obj) +{ + RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); + RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); + return RSHAPE(RBASIC_SHAPE_ID(obj))->next_field_index; +} + +static inline uint32_t +ROBJECT_FIELDS_COUNT(VALUE obj) +{ + if (rb_obj_shape_complex_p(obj)) { + return ROBJECT_FIELDS_COUNT_COMPLEX(obj); + } + else { + return ROBJECT_FIELDS_COUNT_NOT_COMPLEX(obj); + } +} + +static inline VALUE +ROBJECT_FIELDS_OBJ(VALUE obj) +{ + RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); + + return rb_obj_shape_embedded_p(obj) ? obj : ROBJECT(obj)->as.extended; +} + +static inline VALUE * +ROBJECT_EMBEDDED_FIELDS(VALUE obj) +{ + return ROBJECT(obj)->as.ary; +} + +static inline VALUE * +ROBJECT_FIELDS(VALUE obj) +{ + RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); + + return ROBJECT_EMBEDDED_FIELDS(ROBJECT_FIELDS_OBJ(obj)); +} + #endif diff --git a/variable.c b/variable.c index c29054b57652d2..431e0a94540fe0 100644 --- a/variable.c +++ b/variable.c @@ -1710,10 +1710,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) if (fields_obj != original_fields_obj) { switch (type) { case T_OBJECT: - if (!fields_obj) { - FL_UNSET_RAW(obj, ROBJECT_HEAP); - } - else if (fields_obj != obj) { + if (fields_obj && fields_obj != obj) { ROBJECT_SET_EXTENDED(obj, fields_obj); } break; diff --git a/yjit.c b/yjit.c index 99375a1cdac876..7999c10d302c69 100644 --- a/yjit.c +++ b/yjit.c @@ -439,6 +439,12 @@ rb_yjit_shape_obj_complex_p(VALUE obj) return rb_obj_shape_complex_p(obj); } +bool +rb_yjit_shape_obj_embedded_p(VALUE obj) +{ + return rb_obj_shape_embedded_p(obj); +} + attr_index_t rb_yjit_shape_capacity(shape_id_t shape_id) { diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index 373d5abb42c835..4352625a57c896 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -88,6 +88,7 @@ fn main() { .allowlist_function("rb_shape_get_iv_index") .allowlist_function("rb_shape_transition_add_ivar_no_warnings") .allowlist_function("rb_yjit_shape_obj_complex_p") + .allowlist_function("rb_yjit_shape_obj_embedded_p") .allowlist_function("rb_yjit_shape_capacity") .allowlist_function("rb_yjit_shape_index") .allowlist_var("SHAPE_ID_NUM_BITS") diff --git a/yjit/src/cruby.rs b/yjit/src/cruby.rs index 8b99a8154a665f..a5514115f11ad8 100644 --- a/yjit/src/cruby.rs +++ b/yjit/src/cruby.rs @@ -453,9 +453,7 @@ impl VALUE { } pub fn embedded_p(self) -> bool { - unsafe { - FL_TEST_RAW(self, VALUE(ROBJECT_HEAP as usize)) == VALUE(0) - } + unsafe { rb_yjit_shape_obj_embedded_p(self) } } pub fn as_isize(self) -> isize { diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 48569908056e46..72152ae278e3c7 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -271,8 +271,6 @@ pub const RARRAY_EMBED_LEN_SHIFT: ruby_rarray_consts = 15; pub type ruby_rarray_consts = u32; pub const RMODULE_IS_REFINEMENT: ruby_rmodule_flags = 8192; pub type ruby_rmodule_flags = u32; -pub const ROBJECT_HEAP: ruby_robject_flags = 65536; -pub type ruby_robject_flags = u32; pub type rb_block_call_func = ::std::option::Option< unsafe extern "C" fn( yielded_arg: VALUE, @@ -1219,6 +1217,7 @@ extern "C" { ); pub fn rb_object_shape_count() -> VALUE; pub fn rb_yjit_shape_obj_complex_p(obj: VALUE) -> bool; + pub fn rb_yjit_shape_obj_embedded_p(obj: VALUE) -> bool; pub fn rb_yjit_shape_capacity(shape_id: shape_id_t) -> attr_index_t; pub fn rb_yjit_shape_index(shape_id: shape_id_t) -> attr_index_t; pub fn rb_yjit_sendish_sp_pops(ci: *const rb_callinfo) -> usize; diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 74f8e676a8acd4..ec897ac538f15f 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -648,10 +648,8 @@ impl VALUE { } } - pub fn embedded_p(self) -> bool { - unsafe { - FL_TEST_RAW(self, VALUE(ROBJECT_HEAP as usize)) == VALUE(0) - } + pub fn layout(self) -> ShapeLayout { + self.shape_id_of().layout() } pub fn struct_embedded_p(self) -> bool { diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 6ebcbd5335d5a6..232478f1220fb1 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -367,8 +367,6 @@ pub struct RArray__bindgen_ty_1__bindgen_ty_1__bindgen_ty_1 { } pub const RMODULE_IS_REFINEMENT: ruby_rmodule_flags = 8192; pub type ruby_rmodule_flags = u32; -pub const ROBJECT_HEAP: ruby_robject_flags = 65536; -pub type ruby_robject_flags = u32; pub type rb_event_flag_t = u32; pub type rb_block_call_func = ::std::option::Option< unsafe extern "C" fn( diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index c374e98c85c9e1..7ef481ef08873b 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6706,7 +6706,7 @@ mod hir_opt_tests { TEST = ExtendedSetIvar.instance_method(:test) OBJ "#); - assert!(!obj.embedded_p()); + assert!(obj.layout() == ShapeLayout::Extended); assert_snapshot!(hir_string_proc("TEST"), @" fn test@:4: @@ -17016,7 +17016,7 @@ mod hir_opt_tests { OBJ "#); // Skip builds where five ivars already force heap-backed storage. - if !obj.embedded_p() { + if obj.layout() != ShapeLayout::RObject { return; } @@ -17027,7 +17027,7 @@ mod hir_opt_tests { probe.instance_variable_set(:@overflow, 1) probe "#); - if probe.embedded_p() { + if probe.layout() == ShapeLayout::RObject { return; } eval("OBJ.test"); diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index e3b89c9319ebe7..edf93dffa0b834 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -332,7 +332,7 @@ impl ProfiledType { flags: Flags::immediate() }; } let mut flags = Flags::none(); - if obj.embedded_p() { + if obj.shape_id_of().layout() == ShapeLayout::RObject { flags.0 |= Flags::IS_EMBEDDED; } if obj.struct_embedded_p() { From eef14ff26e9141c9a295b070280de60928825dbd Mon Sep 17 00:00:00 2001 From: Ryan Davis Date: Sun, 12 Jul 2026 15:39:12 -0700 Subject: [PATCH 04/39] [ruby/prism] ruby_parser: require timeout since it uses it. https://github.com/ruby/prism/commit/59dfa96010 --- lib/prism/translation/ruby_parser.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/prism/translation/ruby_parser.rb b/lib/prism/translation/ruby_parser.rb index 42bc5ee658dec7..54decd5bf3918e 100644 --- a/lib/prism/translation/ruby_parser.rb +++ b/lib/prism/translation/ruby_parser.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true # :markup: markdown +require "timeout" + begin require "sexp" rescue LoadError From 5840692b0dfbf740e565e7f7bd72d2d0c8e24852 Mon Sep 17 00:00:00 2001 From: Ryan Davis Date: Sun, 12 Jul 2026 15:39:55 -0700 Subject: [PATCH 05/39] [ruby/prism] ruby_parser: add ability to track scopes across parses. https://github.com/ruby/prism/commit/6c1c65fcf0 --- lib/prism/translation/ruby_parser.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/prism/translation/ruby_parser.rb b/lib/prism/translation/ruby_parser.rb index 54decd5bf3918e..da6f02cd200a4f 100644 --- a/lib/prism/translation/ruby_parser.rb +++ b/lib/prism/translation/ruby_parser.rb @@ -1625,17 +1625,23 @@ def visit_write_value(node) end private_constant :Compiler + attr_accessor :scopes # :nodoc: + + def initialize scopes:nil # :nodoc: + super() + self.scopes = [scopes] if scopes + end # Parse the given source and translate it into the seattlerb/ruby_parser # gem's Sexp format. def parse(source, filepath = "(string)") - translate(Prism.parse(source, filepath: filepath, partial_script: true), filepath) + translate(Prism.parse(source, filepath: filepath, partial_script: true, scopes: scopes), filepath) end # Parse the given file and translate it into the seattlerb/ruby_parser # gem's Sexp format. def parse_file(filepath) - translate(Prism.parse_file(filepath, partial_script: true), filepath) + translate(Prism.parse_file(filepath, partial_script: true, scopes: scopes), filepath) end # Parse the give file and translate it into the @@ -1649,14 +1655,14 @@ def process(ruby, file = "(string)", timeout = nil) class << self # Parse the given source and translate it into the seattlerb/ruby_parser # gem's Sexp format. - def parse(source, filepath = "(string)") - new.parse(source, filepath) + def parse(source, filepath = "(string)", scopes: nil) + new(scopes: scopes).parse(source, filepath) end # Parse the given file and translate it into the seattlerb/ruby_parser # gem's Sexp format. - def parse_file(filepath) - new.parse_file(filepath) + def parse_file(filepath, scopes: nil) + new(scopes: scopes).parse_file(filepath) end end From d81da865f8fc09657c1fefbb37ae771e3aba2f2f Mon Sep 17 00:00:00 2001 From: Ryan Davis Date: Sun, 12 Jul 2026 15:40:51 -0700 Subject: [PATCH 06/39] [ruby/prism] ruby_parser: add ability to subclass compiler to override methods. https://github.com/ruby/prism/commit/201f6a1212 --- lib/prism/translation/ruby_parser.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/prism/translation/ruby_parser.rb b/lib/prism/translation/ruby_parser.rb index da6f02cd200a4f..505d6c21f86497 100644 --- a/lib/prism/translation/ruby_parser.rb +++ b/lib/prism/translation/ruby_parser.rb @@ -1562,7 +1562,7 @@ def attach_comments(sexp, node) # Create a new compiler with the given options. def copy_compiler(in_def: self.in_def, in_pattern: self.in_pattern) - Compiler.new(file, in_def: in_def, in_pattern: in_pattern) + self.class.new(file, in_def: in_def, in_pattern: in_pattern) end # Create a new Sexp object from the given prism node and arguments. @@ -1624,7 +1624,6 @@ def visit_write_value(node) end end - private_constant :Compiler attr_accessor :scopes # :nodoc: def initialize scopes:nil # :nodoc: @@ -1677,7 +1676,7 @@ def translate(result, filepath) end result.attach_comments! - result.value.accept(Compiler.new(filepath)) + result.value.accept(self.class::Compiler.new(filepath)) end end end From a706e28800252c8a9d6ae2a293d2d81bee6bcb0b Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 15 Jul 2026 10:03:25 -0400 Subject: [PATCH 07/39] [ruby/prism] Use same type for scopes across translators https://github.com/ruby/prism/commit/bf753a1f7a --- lib/prism/translation/ruby_parser.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/prism/translation/ruby_parser.rb b/lib/prism/translation/ruby_parser.rb index 505d6c21f86497..3602b65ddb3c55 100644 --- a/lib/prism/translation/ruby_parser.rb +++ b/lib/prism/translation/ruby_parser.rb @@ -1624,11 +1624,13 @@ def visit_write_value(node) end end - attr_accessor :scopes # :nodoc: + # Optional scopes to pass to the parser. + attr_reader :scopes #: Array[Array[Symbol]]? - def initialize scopes:nil # :nodoc: + #: (?scopes: Array[Array[Symbol]]?) -> void + def initialize(scopes: nil) super() - self.scopes = [scopes] if scopes + @scopes = scopes end # Parse the given source and translate it into the seattlerb/ruby_parser From 65bb510a276d6dbfbe7378292f95b05e2347d329 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 15 Jul 2026 12:53:15 +0200 Subject: [PATCH 08/39] gc.c: Refactor rb_gc_obj_needs_cleanup_p --- gc.c | 58 ++++++++++++++++++++-------------------------------------- 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/gc.c b/gc.c index c41e7867fba688..69f3345af82729 100644 --- a/gc.c +++ b/gc.c @@ -1404,22 +1404,21 @@ rb_gc_obj_needs_cleanup_p(VALUE obj) if (flags & FL_FINALIZE) return true; - switch (flags & RUBY_T_MASK) { - case T_IMEMO: + if ((flags & RUBY_T_MASK) == T_IMEMO) { return rb_gc_imemo_needs_cleanup_p(obj); + } - case T_DATA: - case T_OBJECT: - case T_STRING: - case T_ARRAY: - case T_HASH: - case T_BIGNUM: - case T_STRUCT: + shape_id_t shape_id = RBASIC_SHAPE_ID(obj); + if (rb_shape_has_fields(shape_id) && rb_shape_layout(shape_id) == SHAPE_ID_LAYOUT_OTHER) { + return true; + } + + switch (flags & RUBY_T_MASK) { case T_FLOAT: case T_RATIONAL: case T_COMPLEX: - case T_MATCH: - break; + case T_OBJECT: + return false; case T_FILE: case T_SYMBOL: @@ -1428,13 +1427,9 @@ rb_gc_obj_needs_cleanup_p(VALUE obj) case T_MODULE: case T_REGEXP: return true; - } - - shape_id_t shape_id = RBASIC_SHAPE_ID(obj); - switch (flags & RUBY_T_MASK) { - case T_OBJECT: - return false; + case T_IMEMO: + UNREACHABLE_RETURN(true); case T_DATA: { @@ -1449,38 +1444,25 @@ rb_gc_obj_needs_cleanup_p(VALUE obj) return true; case T_STRING: - if (flags & (RSTRING_NOEMBED | RSTRING_FSTR)) return true; - return rb_shape_has_fields(shape_id); + return (flags & (RSTRING_NOEMBED | RSTRING_FSTR)); case T_ARRAY: - if (!(flags & RARRAY_EMBED_FLAG)) return true; - return rb_shape_has_fields(shape_id); + return !(flags & RARRAY_EMBED_FLAG); case T_HASH: - if (flags & RHASH_ST_TABLE_FLAG) return true; - return rb_shape_has_fields(shape_id); + return !(flags & RHASH_ST_TABLE_FLAG); case T_MATCH: - if ((flags & (RMATCH_ONIG | RMATCH_OFFSETS_EXTERNAL)) || USE_DEBUG_COUNTER) return true; - return rb_shape_has_fields(shape_id); + return !((flags & (RMATCH_ONIG | RMATCH_OFFSETS_EXTERNAL)) || USE_DEBUG_COUNTER); case T_BIGNUM: - if (!(flags & BIGNUM_EMBED_FLAG)) return true; - return rb_shape_has_fields(shape_id); + return !(flags & BIGNUM_EMBED_FLAG); case T_STRUCT: - if (!(flags & RSTRUCT_EMBED_LEN_MASK)) return true; - if (flags & RSTRUCT_GEN_FIELDS) return rb_shape_has_fields(shape_id); - return false; - - case T_FLOAT: - case T_RATIONAL: - case T_COMPLEX: - return rb_shape_has_fields(shape_id); - - default: - UNREACHABLE_RETURN(true); + return !(flags & RSTRUCT_EMBED_LEN_MASK); } + + UNREACHABLE_RETURN(true); } static void From cd5e5c093f64174d729ef50fd294817af4bda989 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Jul 2026 23:56:07 +0900 Subject: [PATCH 09/39] [Feature #22185] [DOC] Add the reference --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 6392e1ba479187..aa67a7ed80a672 100644 --- a/NEWS.md +++ b/NEWS.md @@ -245,6 +245,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #21981]: https://bugs.ruby-lang.org/issues/21981 [Feature #22137]: https://bugs.ruby-lang.org/issues/22137 [Feature #22139]: https://bugs.ruby-lang.org/issues/22139 +[Feature #22185]: https://bugs.ruby-lang.org/issues/22185 [PR #17201]: https://github.com/ruby/ruby/pull/17201 [RubyGems-v4.0.4]: https://github.com/rubygems/rubygems/releases/tag/v4.0.4 [RubyGems-v4.0.5]: https://github.com/rubygems/rubygems/releases/tag/v4.0.5 From 92e4ea76960cdaad4dc25d3b8375e755db999884 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 16 Jul 2026 00:13:07 +0900 Subject: [PATCH 10/39] [DOC] Mark the undocumented method --- lib/prism/translation/ruby_parser.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/prism/translation/ruby_parser.rb b/lib/prism/translation/ruby_parser.rb index 3602b65ddb3c55..edb3f3bf04d6e8 100644 --- a/lib/prism/translation/ruby_parser.rb +++ b/lib/prism/translation/ruby_parser.rb @@ -1627,6 +1627,7 @@ def visit_write_value(node) # Optional scopes to pass to the parser. attr_reader :scopes #: Array[Array[Symbol]]? + # :nodoc: #: (?scopes: Array[Array[Symbol]]?) -> void def initialize(scopes: nil) super() From 513875464f0b71ad6c311ef6ae469eb935d5d7ca Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 2 Jul 2026 19:24:42 +0900 Subject: [PATCH 11/39] [Feature #22175] Add `Range#clamp` Return a new Range with its begin and end clamped to given bounds, either as `clamp(min, max)` or `clamp(range)`. --- NEWS.md | 6 ++ range.c | 119 +++++++++++++++++++++++ spec/ruby/core/range/clamp_spec.rb | 83 ++++++++++++++++ spec/ruby/core/range/fixtures/classes.rb | 14 +++ test/ruby/test_range.rb | 66 +++++++++++++ 5 files changed, 288 insertions(+) create mode 100644 spec/ruby/core/range/clamp_spec.rb diff --git a/NEWS.md b/NEWS.md index aa67a7ed80a672..56351a5c2cbcfd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -64,6 +64,11 @@ Note: We're only listing outstanding class updates. ineffect inside its body, without affecting the original Proc. [[Feature #22097]] +* Range + + * `Range#clamp` is added. It returns a new `Range` instance whose + begin and end values are clamped to the given bounds. [[Feature #22175]] + * Regexp * All instances of `Regexp` are now frozen, not just literals. @@ -245,6 +250,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #21981]: https://bugs.ruby-lang.org/issues/21981 [Feature #22137]: https://bugs.ruby-lang.org/issues/22137 [Feature #22139]: https://bugs.ruby-lang.org/issues/22139 +[Feature #22175]: https://bugs.ruby-lang.org/issues/22175 [Feature #22185]: https://bugs.ruby-lang.org/issues/22185 [PR #17201]: https://github.com/ruby/ruby/pull/17201 [RubyGems-v4.0.4]: https://github.com/rubygems/rubygems/releases/tag/v4.0.4 diff --git a/range.c b/range.c index 041cd93f73ee4e..fba73f4a39d1c3 100644 --- a/range.c +++ b/range.c @@ -192,6 +192,18 @@ range_eq(VALUE range, VALUE obj) return rb_exec_recursive_paired(recursive_equal, range, obj, obj); } +/* compares _a_ and _b_ and returns: + * < 0: a < b + * = 0: a = b + * > 0: a > b + * raises an ArgumentError if non-comparable + */ +static int +r_cmp(VALUE a, VALUE b) +{ + return OPTIMIZED_CMP(a, b); +} + /* compares _a_ and _b_ and returns: * < 0: a < b * = 0: a = b @@ -2594,6 +2606,111 @@ range_overlap(VALUE range, VALUE other) return Qtrue; } +/* + * call-seq: + * clamp(min, max) -> range + * clamp(range) -> range + * + * Returns a new +Range+ instance whose begin and end values are + * clamped to _min_ and _max_, or to _range.begin_ and _range.end_. + * + * The returned range excludes its end if any of the following is true: + * + * - The returned end value is an excluded end value of +self+ or _range_. + * - Both begin and end values are clamped to the lower bound, or both + * are clamped to the upper bound. Since +self+ is entirely outside + * the clamping bounds, the returned range is made empty by + * excluding its end. + * + * Otherwise, the returned range includes its end. + * + * Examples: + * + * (1..10).clamp(3, 7) # => 3..7 + * (1...10).clamp(3, 7) # => 3..7 + * (1...10).clamp(3, 10) # => 3...10 + * (0...).clamp(0, 10) # => 0..10 + * + * (1..10).clamp(3..7) # => 3..7 + * (1..10).clamp(3...7) # => 3...7 + * (1..5).clamp(3...7) # => 3..5 + * + * (..10).clamp(3, 7) # => 3..7 + * (...10).clamp(3, 7) # => 3..7 + * (..10).clamp(3...7) # => 3...7 + * (..5).clamp(3...7) # => 3..5 + * + * (1..10).clamp(20..30) # => 20...20 + * (1..10).clamp(-10..0) # => 0...0 + * (..10).clamp(20..30) # => 20...20 + * + * (1..10).clamp(..7) # => 1..7 + * (1..10).clamp(...7) # => 1...7 + * (1..5).clamp(...7) # => 1..5 + * + * (1..10).clamp(3..) # => 3..10 + * (1..10).clamp(3...) # => 3..10 + * (1..).clamp(3..) # => 3.. + * (1...).clamp(3...) # => 3... + */ + +static VALUE +range_clamp(int argc, VALUE *argv, VALUE self) +{ + VALUE self_beg = RANGE_BEG(self); + VALUE self_end = RANGE_END(self); + int self_excl = EXCL(self); + VALUE min, max; + int clamp_beg = 0, clamp_end = 0, excl = 0; + + argc = rb_scan_args(argc, argv, "11", &min, &max); + if (argc == 1) { + VALUE range = min; + if (!rb_range_values(range, &min, &max, &excl)) { + rb_raise(rb_eTypeError, "wrong argument type %s (expected Range)", + rb_builtin_class_name(range)); + } + } + if (!NIL_P(min) && !NIL_P(max) && r_cmp(min, max) > 0) { + rb_raise(rb_eArgError, "min argument must be less than or equal to max argument"); + } + + if (!NIL_P(min)) { + if (NIL_P(self_beg) || r_cmp(self_beg, min) < 0) { + clamp_beg = -1; + self_beg = min; + } + if (!NIL_P(self_end) && r_cmp(self_end, min) < 0) { + clamp_end = -1; + self_end = min; + } + } + if (!NIL_P(max)) { + if (clamp_beg == 0) { + if (!NIL_P(self_beg) && r_cmp(self_beg, max) > 0) { + clamp_beg = +1; + self_beg = max; + } + } + if (clamp_end == 0) { + int cmp = NIL_P(self_end) ? +1 : r_cmp(self_end, max); + if (cmp > 0) { + clamp_end = +1; + self_end = max; + self_excl = excl; + } + else if (cmp == 0) { + self_excl |= excl; + } + } + } + if (clamp_beg && clamp_beg == clamp_end) { + /* self is entirely outside the clamping bounds. */ + self_excl = TRUE; + } + return rb_range_new(self_beg, self_end, self_excl); +} + /* A \Range object represents a collection of values * that are between given begin and end values. * @@ -2783,6 +2900,7 @@ range_overlap(VALUE range, VALUE other) * === Methods for Creating a \Range * * - ::new: Returns a new range. + * - #clamp: Returns a new range with clamped begin and end values. * * === Methods for Querying * @@ -2878,4 +2996,5 @@ Init_Range(void) rb_define_method(rb_cRange, "cover?", range_cover, 1); rb_define_method(rb_cRange, "count", range_count, -1); rb_define_method(rb_cRange, "overlap?", range_overlap, 1); + rb_define_method(rb_cRange, "clamp", range_clamp, -1); } diff --git a/spec/ruby/core/range/clamp_spec.rb b/spec/ruby/core/range/clamp_spec.rb new file mode 100644 index 00000000000000..e50b4d246ae7f1 --- /dev/null +++ b/spec/ruby/core/range/clamp_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +ruby_version_is "4.1" do + describe "Range#clamp" do + it "clamps begin and end to min and max" do + (1..10).clamp(3, 7).should == (3..7) + (..10).clamp(3, 7).should == (3..7) + end + + it "clamps begin and end to a Range" do + (1..10).clamp(3..7).should == (3..7) + (1..5).clamp(3...7).should == (3..5) + (1..10).clamp(RangeSpecs::DuckRange.new(3, 7, true)).should == (3...7) + end + + it "treats min and max like an inclusive Range" do + (1...10).clamp(3, 7).should == (1...10).clamp(3..7) + (1...10).clamp(3, 10).should == (1...10).clamp(3..10) + end + + it "excludes the end when the returned end is excluded by self or the argument Range" do + (1...10).clamp(3, 10).should == (3...10) + (1...10).clamp(3..10).should == (3...10) + (1..10).clamp(3...10).should == (3...10) + (..10).clamp(3...10).should == (3...10) + end + + it "includes the end when the returned end is not an excluded end" do + (1...10).clamp(3, 7).should == (3..7) + (0...).clamp(0, 10).should == (0..10) + (1..5).clamp(3...7).should == (3..5) + (..5).clamp(3...7).should == (3..5) + (1..10).clamp(3...).should == (3..10) + end + + it "handles beginless and endless argument Ranges" do + (1..10).clamp(..7).should == (1..7) + (1..10).clamp(...7).should == (1...7) + (1..5).clamp(...7).should == (1..5) + + (1..10).clamp(3..).should == (3..10) + (1..).clamp(3..).should == (3..) + (1...).clamp(3...).should == (3...) + end + + it "returns an inclusive point Range when begin and end are clamped to different equal bounds" do + (1..10).clamp(3, 3).should == (3..3) + (1..10).clamp(3..3).should == (3..3) + end + + it "returns an empty Range when begin and end are clamped to the same side" do + (1..10).clamp(20, 30).should == (20...20) + (1..10).clamp(-10, 0).should == (0...0) + (..10).clamp(20, 30).should == (20...20) + (1..10).clamp(20..30).should == (20...20) + (1..10).clamp(-10..0).should == (0...0) + (..10).clamp(20..30).should == (20...20) + (1..2).clamp(3, 3).should == (3...3) + (4..10).clamp(3, 3).should == (3...3) + (1..).clamp(nil, 0).should == (0...0) + (1..).clamp(..0).should == (0...0) + end + + it "returns a Range instance, not an instance of a subclass" do + RangeSpecs::MyRange.new(1, 10).clamp(3, 7).should.instance_of?(Range) + end + + it "raises ArgumentError when min and max are not comparable" do + -> { (1..3).clamp(1, "z") }.should.raise(ArgumentError) + -> { (1..3).clamp("a", "z") }.should.raise(ArgumentError) + end + + it "raises ArgumentError when min is greater than max" do + -> { (1..3).clamp(2, 1) }.should.raise(ArgumentError) + -> { (1..3).clamp(2..1) }.should.raise(ArgumentError) + end + + it "raises TypeError when the single argument is not a Range" do + -> { (1..3).clamp(1) }.should.raise(TypeError) + end + end +end diff --git a/spec/ruby/core/range/fixtures/classes.rb b/spec/ruby/core/range/fixtures/classes.rb index bdabfb0d1c8176..5176cd42032226 100644 --- a/spec/ruby/core/range/fixtures/classes.rb +++ b/spec/ruby/core/range/fixtures/classes.rb @@ -156,6 +156,20 @@ def inspect class MyRange < Range end + class DuckRange + def initialize(beg, end_, exclude_end = false) + @begin = beg + @end = end_ + @exclude_end = exclude_end + end + + attr_reader :begin, :end + + def exclude_end? + @exclude_end + end + end + class ComparisonError < RuntimeError end end diff --git a/test/ruby/test_range.rb b/test/ruby/test_range.rb index ff17dca69eeb96..e104f4a80bb3d7 100644 --- a/test/ruby/test_range.rb +++ b/test/ruby/test_range.rb @@ -1548,4 +1548,70 @@ def test_overlap? assert_not_operator((1...3), :overlap?, (3..4)) assert_not_operator((...3), :overlap?, (3..)) end + + def test_clamp + # Clamp begin and end to min and max. + assert_equal(3..7, (1..10).clamp(3, 7)) + assert_equal(3..7, (..10).clamp(3, 7)) + + # Clamp begin and end to a range. + assert_equal(3..7, (1..10).clamp(3..7)) + assert_equal(3..5, (1..5).clamp(3...7)) + assert_equal(3...7, (1..10).clamp(DuckRange.new(3, 7, true))) + + # Treat min and max like an inclusive range. + assert_equal((1...10).clamp(3..7), (1...10).clamp(3, 7)) + assert_equal((1...10).clamp(3..10), (1...10).clamp(3, 10)) + + # Exclude the end when the returned end is excluded by self or the + # argument range. + assert_equal(3...10, (1...10).clamp(3, 10)) + assert_equal(3...10, (1...10).clamp(3..10)) + assert_equal(3...10, (1..10).clamp(3...10)) + assert_equal(3...10, (..10).clamp(3...10)) + + # Include the end when the returned end is not an excluded end. + assert_equal(3..7, (1...10).clamp(3, 7)) + assert_equal(0..10, (0...).clamp(0, 10)) + assert_equal(3..5, (1..5).clamp(3...7)) + assert_equal(3..5, (..5).clamp(3...7)) + assert_equal(3..10, (1..10).clamp(3...)) + + # Handle beginless and endless argument ranges. + assert_equal(1..7, (1..10).clamp(..7)) + assert_equal(1...7, (1..10).clamp(...7)) + assert_equal(1..5, (1..5).clamp(...7)) + + assert_equal(3..10, (1..10).clamp(3..)) + assert_equal(3.., (1..).clamp(3..)) + assert_equal(3..., (1...).clamp(3...)) + + # Return an inclusive point range when begin and end are clamped to + # different equal bounds. + assert_equal(3..3, (1..10).clamp(3, 3)) + assert_equal(3..3, (1..10).clamp(3..3)) + + # Return an empty range when begin and end are clamped to the same side. + assert_equal(20...20, (1..10).clamp(20, 30)) + assert_equal(0...0, (1..10).clamp(-10, 0)) + assert_equal(20...20, (..10).clamp(20, 30)) + assert_equal(20...20, (1..10).clamp(20..30)) + assert_equal(0...0, (1..10).clamp(-10..0)) + assert_equal(20...20, (..10).clamp(20..30)) + assert_equal(3...3, (1..2).clamp(3, 3)) + assert_equal(3...3, (4..10).clamp(3, 3)) + assert_equal(0...0, (1..).clamp(nil, 0)) + assert_equal(0...0, (1..).clamp(..0)) + + # Return a Range instance, not an instance of a subclass. + subclass = Class.new(Range) + assert_instance_of(Range, subclass.new(1, 10).clamp(3, 7)) + + # Raise for invalid bounds or argument types. + assert_raise(ArgumentError) {(1..3).clamp(1, "z")} + assert_raise(ArgumentError) {(1..3).clamp("a", "z")} + assert_raise(ArgumentError) {(1..3).clamp(2, 1)} + assert_raise(ArgumentError) {(1..3).clamp(2..1)} + assert_raise(TypeError) {(1..3).clamp(1)} + end end From b061f5c74a17c9dc13c7a02c506ba6799d17376d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 16 Jul 2026 01:15:04 +0900 Subject: [PATCH 12/39] [ruby/erb] Fix wrong receiver in eoutvar example for #src (https://github.com/ruby/erb/pull/123) The example was updated to assign the ERB instance to `erb` instead of `template`, but the following line still called `template.src`. Since `template` is a String there, the example raises NoMethodError. https://github.com/ruby/erb/commit/fbe832ac5c Co-authored-by: Claude Fable 5 --- lib/erb.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/erb.rb b/lib/erb.rb index bde90de84122ce..fbd146ed2ed57f 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -889,7 +889,7 @@ def make_compiler(trim_mode) # # ``` # erb = ERB.new(template, eoutvar: '_foo') - # puts template.src.split('; ') + # puts erb.src.split('; ') # #coding:UTF-8 # _foo = +'' # _foo.<< "The time is ".freeze From d90de908b375c6590a1cd8bf5e10ebd17242fe20 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 15 Jul 2026 09:36:33 -0700 Subject: [PATCH 13/39] ZJIT: Write full C method names in stats (#17895) Makes methods look more like `Array#[]` than just `[]`. Also use a thunk instead of allocating the string all the time. --- zjit/src/backend/lir.rs | 9 ++++++++- zjit/src/codegen.rs | 12 ++++++------ zjit/src/hir.rs | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index f47fa06bdd5038..3012d602b1e55c 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -3741,11 +3741,18 @@ impl Assembler { } pub fn count_call_to(&mut self, fn_name: &str) { + self.count_call_to_with(|| fn_name.to_string()); + } + + /// Like [`Assembler::count_call_to`], but only builds the name when stats are + /// enabled. Use for callers where computing the name is non-trivial (e.g. + /// resolving a `Class#method` string) so no-stats builds don't pay for it. + pub fn count_call_to_with(&mut self, fn_name: impl FnOnce() -> String) { // We emit ccalls while initializing the JIT. Unfortunately, we skip those because // otherwise we have no counter pointers to read. if crate::state::ZJITState::has_instance() && get_option!(stats) { let ccall_counter_pointers = crate::state::ZJITState::get_ccall_counter_pointers(); - let counter_ptr = ccall_counter_pointers.entry(fn_name.to_string()).or_insert_with(|| Box::new(0)); + let counter_ptr = ccall_counter_pointers.entry(fn_name()).or_insert_with(|| Box::new(0)); let counter_ptr: &mut u64 = counter_ptr.as_mut(); self.incr_counter(Opnd::const_ptr(counter_ptr), 1.into()); } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 20ff39339b7352..76f9954a600d6a 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -23,7 +23,7 @@ use crate::stats::{counter_ptr, with_time_stat, trace_compile_phase, Counter, Co use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; use crate::backend::lir::{self, Assembler, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NATIVE_BASE_PTR, Opnd, SP, SideExit, SideExitRecompile, SideExitTarget, StackMap, StackMapEntry, Target, asm_ccall, asm_comment}; use crate::hir::{iseq_to_hir, BlockId, Invariant, RangeType, SideExitReason::{self, *}, SpecialBackrefSymbol, SpecialObjectType}; -use crate::hir::{BlockHandler, CCallVariadicData, CCallWithFrameData, Const, FieldName, FrameState, Function, Insn, InsnId, Recompile, SendDirectData, SendFallbackReason}; +use crate::hir::{BlockHandler, CCallVariadicData, CCallWithFrameData, Const, FieldName, FrameState, Function, Insn, InsnId, Recompile, SendDirectData, SendFallbackReason, qualified_method_name}; use crate::hir_type::{types, Type}; use crate::options::{get_option, InlineDepth, PerfMap, DEFAULT_MAX_VERSIONS}; use crate::cast::IntoUsize; @@ -726,7 +726,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::GuardLess { left, right, ref reason, state } => gen_guard_less(jit, asm, function, opnd!(left), opnd!(right), **reason, &function.frame_state(state)), &Insn::GuardGreaterEq { left, right, state, .. } => gen_guard_greater_eq(jit, asm, function, opnd!(left), opnd!(right), &function.frame_state(state)), Insn::PatchPoint { invariant, state } => no_output!(gen_patch_point(jit, asm, function, invariant, &function.frame_state(*state))), - Insn::CCall { cfunc, recv, args, name, owner: _, return_type: _, elidable: _ } => gen_ccall(asm, *cfunc, *name, opnd!(recv), opnds!(args)), + Insn::CCall { cfunc, recv, args, name, owner, return_type: _, elidable: _ } => gen_ccall(asm, *cfunc, *name, *owner, opnd!(recv), opnds!(args)), Insn::CCallWithFrame(insn) => { let CCallWithFrameData { cfunc, recv, name, args, cme, state, block, .. } = &**insn; gen_ccall_with_frame(jit, asm, function, *cfunc, *name, opnd!(recv), opnds!(args), *cme, *block, &function.frame_state(*state)) @@ -1079,7 +1079,7 @@ fn gen_ccall_with_frame( let mut cfunc_args = vec![recv]; cfunc_args.extend(args); - asm.count_call_to(&name.contents_lossy()); + asm.count_call_to_with(|| qualified_method_name(unsafe { (*cme).owner }, name)); let result = asm.ccall(cfunc, cfunc_args); asm_comment!(asm, "pop C frame"); @@ -1096,10 +1096,10 @@ fn gen_ccall_with_frame( /// Lowering for [`Insn::CCall`]. This is a low-level raw call that doesn't know /// anything about the callee, so handling for e.g. GC safety is dealt with elsewhere. -fn gen_ccall(asm: &mut Assembler, cfunc: *const u8, name: ID, recv: Opnd, args: Vec) -> lir::Opnd { +fn gen_ccall(asm: &mut Assembler, cfunc: *const u8, name: ID, owner: VALUE, recv: Opnd, args: Vec) -> lir::Opnd { let mut cfunc_args = vec![recv]; cfunc_args.extend(args); - asm.count_call_to(&name.contents_lossy()); + asm.count_call_to_with(|| if owner == Qnil { name.contents_lossy().to_string() } else { qualified_method_name(owner, name) }); asm.ccall(cfunc, cfunc_args) } @@ -1168,7 +1168,7 @@ fn gen_ccall_variadic( asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); let argv_ptr = gen_push_opnds(jit, asm, &args); - asm.count_call_to(&name.contents_lossy()); + asm.count_call_to_with(|| qualified_method_name(unsafe { (*cme).owner }, name)); let result = asm.ccall(cfunc, vec![args.len().into(), argv_ptr, recv]); asm_comment!(asm, "pop C frame"); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index ace63943053ce4..eba5b80e850830 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1924,7 +1924,7 @@ fn id_is_empty(id: ID) -> bool { /// Construct a qualified method name for display/debug output. /// Returns strings like "Array#length" for instance methods or "Foo.bar" for singleton methods. -fn qualified_method_name(class: VALUE, method_id: ID) -> String { +pub(crate) fn qualified_method_name(class: VALUE, method_id: ID) -> String { let method_name = method_id.contents_lossy(); // rb_zjit_singleton_class_p also checks if it's a class if unsafe { rb_zjit_singleton_class_p(class) } { From 6cd755deabfa6197676663c2ffc261c52effe875 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 15 Jul 2026 17:47:48 +0200 Subject: [PATCH 14/39] variable.c: generalize use of `rb_obj_fields()` Numerous codepath have the same `switch/case` with very little differing logic compared to `rb_obj_fields`, might as well use it everywhere and share more code. --- variable.c | 200 ++++++++++++++++------------------------------------- 1 file changed, 60 insertions(+), 140 deletions(-) diff --git a/variable.c b/variable.c index 431e0a94540fe0..a73bb8826382cf 100644 --- a/variable.c +++ b/variable.c @@ -1293,27 +1293,34 @@ obj_use_generic_fields_tbl_p(VALUE obj) VALUE rb_obj_fields(VALUE obj, ID field_name) { - RUBY_ASSERT(!RB_TYPE_P(obj, T_IMEMO)); ivar_ractor_check(obj, field_name); - VALUE fields_obj = 0; - if (rb_obj_shape_has_fields(obj)) { - switch (BUILTIN_TYPE(obj)) { - case T_OBJECT: - fields_obj = ROBJECT_FIELDS_OBJ(obj); - break; - case T_DATA: - fields_obj = RTYPEDDATA(obj)->fields_obj; - break; - case T_STRUCT: - if (LIKELY(!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS))) { - fields_obj = RSTRUCT_FIELDS_OBJ(obj); - break; - } - goto generic_fields; - default: - generic_fields: - { + switch (BUILTIN_TYPE(obj)) { + case T_IMEMO: + RUBY_ASSERT(IMEMO_TYPE_P(obj, imemo_fields)); + return obj; + + case T_OBJECT: + return ROBJECT_FIELDS_OBJ(obj); + + case T_CLASS: + case T_MODULE: + return RCLASS_WRITABLE_FIELDS_OBJ(obj); + + case T_DATA: + return RTYPEDDATA(obj)->fields_obj; + + case T_STRUCT: + if (LIKELY(!FL_TEST_RAW(obj, RSTRUCT_GEN_FIELDS))) { + return RSTRUCT_FIELDS_OBJ(obj); + } + goto generic_fields; + default: + generic_fields: + { + VALUE fields_obj = 0; + + if (rb_obj_shape_has_fields(obj)) { rb_execution_context_t *ec = GET_EC(); if (ec->gen_fields_cache.obj == obj && !UNDEF_P(ec->gen_fields_cache.fields_obj) && rb_imemo_fields_owner(ec->gen_fields_cache.fields_obj) == obj) { fields_obj = ec->gen_fields_cache.fields_obj; @@ -1325,9 +1332,10 @@ rb_obj_fields(VALUE obj, ID field_name) ec->gen_fields_cache.obj = obj; } } + + return fields_obj; } } - return fields_obj; } void @@ -1450,24 +1458,7 @@ rb_obj_field_get(VALUE obj, shape_id_t target_shape_id) RUBY_ASSERT(!SPECIAL_CONST_P(obj)); RUBY_ASSERT(RSHAPE_TYPE_P(target_shape_id, SHAPE_IVAR) || RSHAPE_TYPE_P(target_shape_id, SHAPE_OBJ_ID)); - VALUE fields_obj; - - switch (BUILTIN_TYPE(obj)) { - case T_CLASS: - case T_MODULE: - fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(obj); - break; - case T_OBJECT: - fields_obj = ROBJECT_FIELDS_OBJ(obj); - break; - case T_IMEMO: - RUBY_ASSERT(IMEMO_TYPE_P(obj, imemo_fields)); - fields_obj = obj; - break; - default: - fields_obj = rb_obj_fields(obj, RSHAPE_EDGE_NAME(target_shape_id)); - break; - } + VALUE fields_obj = rb_obj_fields(obj, RSHAPE_EDGE_NAME(target_shape_id)); if (UNLIKELY(rb_shape_complex_p(target_shape_id))) { st_table *fields_hash = rb_imemo_fields_complex_tbl(fields_obj); @@ -1486,35 +1477,9 @@ rb_ivar_lookup(VALUE obj, ID id, VALUE undef) { if (SPECIAL_CONST_P(obj)) return undef; - VALUE fields_obj; - - switch (BUILTIN_TYPE(obj)) { - case T_CLASS: - case T_MODULE: - { - VALUE val = rb_ivar_lookup(RCLASS_WRITABLE_FIELDS_OBJ(obj), id, undef); - if (val != undef && - rb_is_instance_id(id) && - UNLIKELY(!rb_ractor_main_p()) && - !rb_ractor_shareable_p(val)) { - rb_raise(rb_eRactorIsolationError, - "can not get unshareable values from instance variables of classes/modules from non-main Ractors (%"PRIsVALUE" from %"PRIsVALUE")", - rb_id2str(id), obj); - } - return val; - } - case T_IMEMO: - // Handled like T_OBJECT - RUBY_ASSERT(IMEMO_TYPE_P(obj, imemo_fields)); - fields_obj = obj; - break; - case T_OBJECT: - fields_obj = ROBJECT_FIELDS_OBJ(obj); - break; - default: - fields_obj = rb_obj_fields(obj, id); - break; - } + int type = BUILTIN_TYPE(obj); + bool is_class = type == T_CLASS || type == T_MODULE; + VALUE fields_obj = rb_obj_fields(obj, is_class ? 0 : id); if (!fields_obj) { return undef; @@ -1522,21 +1487,34 @@ rb_ivar_lookup(VALUE obj, ID id, VALUE undef) shape_id_t shape_id = RBASIC_SHAPE_ID(fields_obj); + VALUE val = undef; if (UNLIKELY(rb_shape_complex_p(shape_id))) { st_table *iv_table = rb_imemo_fields_complex_tbl(fields_obj); - VALUE val; - if (rb_st_lookup(iv_table, (st_data_t)id, (st_data_t *)&val)) { - return val; + if (!rb_st_lookup(iv_table, (st_data_t)id, (st_data_t *)&val)) { + return undef; } - return undef; + } + else { + attr_index_t index = 0; + if (!rb_shape_get_iv_index(shape_id, id, &index)) { + return undef; + } + val = rb_imemo_fields_ptr(fields_obj)[index]; } - attr_index_t index = 0; - if (rb_shape_get_iv_index(shape_id, id, &index)) { - return rb_imemo_fields_ptr(fields_obj)[index]; + if (is_class && val != undef && rb_is_instance_id(id)) { + if (UNLIKELY(!rb_ractor_main_p()) && !rb_ractor_shareable_p(val)) { + rb_raise( + rb_eRactorIsolationError, + "can not get unshareable values from instance variables of classes/modules from " + "non-main Ractors (%"PRIsVALUE" from %"PRIsVALUE")", + rb_id2str(id), + obj + ); + } } - return undef; + return val; } VALUE @@ -1582,18 +1560,7 @@ rb_ivar_get_at_no_ractor_check(VALUE obj, attr_index_t index) { // Used by JITs, but never for T_OBJECT. - VALUE fields_obj; - switch (BUILTIN_TYPE(obj)) { - case T_OBJECT: - UNREACHABLE_RETURN(Qundef); - case T_CLASS: - case T_MODULE: - fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(obj); - break; - default: - fields_obj = rb_obj_fields_no_ractor_check(obj); - break; - } + VALUE fields_obj = rb_obj_fields_no_ractor_check(obj); return rb_imemo_fields_ptr(fields_obj)[index]; } @@ -1624,29 +1591,18 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) rb_check_frozen(obj); VALUE val = undef; - VALUE fields_obj; bool concurrent = false; int type = BUILTIN_TYPE(obj); - switch(type) { - case T_CLASS: - case T_MODULE: + if (type == T_CLASS || type == T_MODULE) { IVAR_ACCESSOR_SHOULD_BE_MAIN_RACTOR(id); - fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(obj); if (rb_multi_ractor_p()) { concurrent = true; } - break; - case T_OBJECT: - fields_obj = ROBJECT_FIELDS_OBJ(obj); - break; - default: { - fields_obj = rb_obj_fields(obj, id); - break; - } } + VALUE fields_obj = rb_obj_fields(obj, id); if (!fields_obj) { return undef; } @@ -2371,51 +2327,15 @@ rb_ivar_count(VALUE obj) if (SPECIAL_CONST_P(obj)) return 0; st_index_t iv_count = 0; - switch (BUILTIN_TYPE(obj)) { - case T_OBJECT: - iv_count = ROBJECT_FIELDS_COUNT(obj); - break; - - case T_CLASS: - case T_MODULE: - { - VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(obj); - if (!fields_obj) { - return 0; - } - if (rb_obj_shape_complex_p(fields_obj)) { - iv_count = rb_st_table_size(rb_imemo_fields_complex_tbl(fields_obj)); - } - else { - iv_count = RBASIC_FIELDS_COUNT(fields_obj); - } - } - break; - - case T_IMEMO: - RUBY_ASSERT(IMEMO_TYPE_P(obj, imemo_fields)); + VALUE fields_obj = rb_obj_fields_no_ractor_check(obj); - if (rb_obj_shape_complex_p(obj)) { - iv_count = rb_st_table_size(rb_imemo_fields_complex_tbl(obj)); + if (fields_obj) { + if (rb_obj_shape_complex_p(fields_obj)) { + iv_count = rb_st_table_size(rb_imemo_fields_complex_tbl(fields_obj)); } else { iv_count = RBASIC_FIELDS_COUNT(obj); } - break; - - default: - { - VALUE fields_obj = rb_obj_fields_no_ractor_check(obj); - if (fields_obj) { - if (rb_obj_shape_complex_p(fields_obj)) { - iv_count = rb_st_table_size(rb_imemo_fields_complex_tbl(fields_obj)); - } - else { - iv_count = RBASIC_FIELDS_COUNT(obj); - } - } - } - break; } if (rb_obj_shape_has_id(obj)) { From e9c87a96288b6e588bfbc4e0fc1f8ed20939edb3 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 15 Jul 2026 10:51:27 -0700 Subject: [PATCH 15/39] ZJIT: Merge send and send_without_block counters (#17721) We only have one `Send` HIR opcode now. Let's not count everything separately. --- zjit.rb | 1 - zjit/src/codegen.rs | 9 ++-- zjit/src/hir.rs | 61 ++++++++------------- zjit/src/hir/opt_tests.rs | 36 ++++++------- zjit/src/stats.rs | 109 +++++++++++--------------------------- 5 files changed, 72 insertions(+), 144 deletions(-) diff --git a/zjit.rb b/zjit.rb index 22eba6e1475260..30ad7f51b75387 100644 --- a/zjit.rb +++ b/zjit.rb @@ -102,7 +102,6 @@ def stats_string # Show fallback counters, ordered by the typical amount of fallbacks for the prefix at the time print_counters_with_prefix(prefix: 'unspecialized_send_def_type_', prompt: 'not optimized method types for send', buf:, stats:, limit: 20) - print_counters_with_prefix(prefix: 'unspecialized_send_without_block_def_type_', prompt: 'not optimized method types for send_without_block', buf:, stats:, limit: 20) print_counters_with_prefix(prefix: 'unspecialized_super_def_type_', prompt: 'not optimized method types for super', buf:, stats:, limit: 20) print_counters_with_prefix(prefix: 'uncategorized_fallback_yarv_insn_', prompt: 'instructions with uncategorized fallback reason', buf:, stats:, limit: 20) print_counters_with_prefix(prefix: 'send_fallback_', prompt: 'send fallback reasons', buf:, stats:, limit: 20) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 76f9954a600d6a..5c66679109372c 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -18,7 +18,7 @@ use crate::invariants::{ use crate::gc::append_gc_offsets; use crate::payload::{IseqCodePtrs, IseqStatus, IseqVersion, IseqVersionRef, JITFrame, get_or_create_iseq_payload}; use crate::state::ZJITState; -use crate::stats::{CompileError, exit_counter_for_compile_error, exit_counter_for_unhandled_hir_insn, incr_counter, incr_counter_by, send_fallback_counter, send_fallback_counter_for_method_type, send_fallback_counter_for_super_method_type, send_fallback_counter_ptr_for_opcode, send_without_block_fallback_counter_for_method_type, send_without_block_fallback_counter_for_optimized_method_type}; +use crate::stats::{CompileError, exit_counter_for_compile_error, exit_counter_for_unhandled_hir_insn, incr_counter, incr_counter_by, send_fallback_counter, send_fallback_counter_for_method_type, send_fallback_counter_for_super_method_type, send_fallback_counter_ptr_for_opcode, send_fallback_counter_for_optimized_method_type}; use crate::stats::{counter_ptr, with_time_stat, trace_compile_phase, Counter, Counter::{compile_time_ns, exit_compile_error}}; use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; use crate::backend::lir::{self, Assembler, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NATIVE_BASE_PTR, Opnd, SP, SideExit, SideExitRecompile, SideExitTarget, StackMap, StackMapEntry, Target, asm_ccall, asm_comment}; @@ -2996,11 +2996,8 @@ fn gen_incr_send_fallback_counter(asm: &mut Assembler, reason: SendFallbackReaso Uncategorized(opcode) => { gen_incr_counter_ptr(asm, send_fallback_counter_ptr_for_opcode(opcode)); } - SendWithoutBlockNotOptimizedMethodType(method_type) => { - gen_incr_counter(asm, send_without_block_fallback_counter_for_method_type(method_type)); - } - SendWithoutBlockNotOptimizedMethodTypeOptimized(method_type) => { - gen_incr_counter(asm, send_without_block_fallback_counter_for_optimized_method_type(method_type)); + SendNotOptimizedMethodTypeOptimized(method_type) => { + gen_incr_counter(asm, send_fallback_counter_for_optimized_method_type(method_type)); } SendNotOptimizedMethodType(method_type) => { gen_incr_counter(asm, send_fallback_counter_for_method_type(method_type)); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index eba5b80e850830..0a031aa9900c90 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -683,17 +683,11 @@ pub enum ReceiverTypeResolution { /// Reason why a send-ish instruction cannot be optimized from a fallback instruction #[derive(Debug, Clone, Copy)] pub enum SendFallbackReason { - SendWithoutBlockPolymorphic, - SendWithoutBlockMegamorphic, - SendWithoutBlockNoProfiles, - SendWithoutBlockCfuncNotVariadic, - SendWithoutBlockCfuncArrayVariadic, - SendWithoutBlockNotOptimizedMethodType(MethodType), - SendWithoutBlockNotOptimizedMethodTypeOptimized(OptimizedMethodType), - SendWithoutBlockNotOptimizedNeedPermission, - SendWithoutBlockBopRedefined, - SendWithoutBlockOperandsNotFixnum, - SendWithoutBlockPolymorphicFallback, + SendCfuncNotVariadic, + SendNotOptimizedMethodTypeOptimized(OptimizedMethodType), + SendBopRedefined, + SendOperandsNotFixnum, + SendPolymorphicFallback, SendDirectKeywordMismatch, SendDirectKeywordCountMismatch, SendDirectMissingKeyword, @@ -759,18 +753,12 @@ pub enum SendFallbackReason { impl Display for SendFallbackReason { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - SendWithoutBlockPolymorphic => write!(f, "SendWithoutBlock: polymorphic call site"), - SendWithoutBlockMegamorphic => write!(f, "SendWithoutBlock: megamorphic call site"), - SendWithoutBlockNoProfiles => write!(f, "SendWithoutBlock: no profile data available"), - SendWithoutBlockCfuncNotVariadic => write!(f, "SendWithoutBlock: C function is not variadic"), - SendWithoutBlockCfuncArrayVariadic => write!(f, "SendWithoutBlock: C function expects array variadic"), - SendWithoutBlockNotOptimizedMethodType(method_type) => write!(f, "SendWithoutBlock: unsupported method type {:?}", method_type), - SendWithoutBlockNotOptimizedMethodTypeOptimized(opt_type) => write!(f, "SendWithoutBlock: unsupported optimized method type {:?}", opt_type), - SendWithoutBlockNotOptimizedNeedPermission => write!(f, "SendWithoutBlock: method private or protected and no FCALL"), + SendCfuncNotVariadic => write!(f, "Send: C function is not variadic"), + SendNotOptimizedMethodTypeOptimized(opt_type) => write!(f, "Send: unsupported optimized method type {:?}", opt_type), SendNotOptimizedNeedPermission => write!(f, "Send: method private or protected and no FCALL"), - SendWithoutBlockBopRedefined => write!(f, "SendWithoutBlock: basic operation was redefined"), - SendWithoutBlockOperandsNotFixnum => write!(f, "SendWithoutBlock: operands are not fixnums"), - SendWithoutBlockPolymorphicFallback => write!(f, "SendWithoutBlock: polymorphic fallback"), + SendBopRedefined => write!(f, "Send: basic operation was redefined"), + SendOperandsNotFixnum => write!(f, "Send: operands are not fixnums"), + SendPolymorphicFallback => write!(f, "Send: polymorphic fallback"), SendDirectKeywordMismatch => write!(f, "SendDirect: keyword mismatch"), SendDirectKeywordCountMismatch => write!(f, "SendDirect: keyword count mismatch"), SendDirectMissingKeyword => write!(f, "SendDirect: missing keyword"), @@ -3872,7 +3860,7 @@ impl Function { fn rewrite_if_frozen(&mut self, block: BlockId, orig_insn_id: InsnId, self_val: InsnId, klass: u32, bop: u32, state: InsnId) { if !unsafe { rb_BASIC_OP_UNREDEFINED_P(bop, klass) } { // If the basic operation is already redefined, we cannot optimize it. - self.set_dynamic_send_reason(orig_insn_id, SendWithoutBlockBopRedefined); + self.set_dynamic_send_reason(orig_insn_id, SendBopRedefined); self.push_insn_id(block, orig_insn_id); return; } @@ -4086,23 +4074,20 @@ impl Function { ReceiverTypeResolution::SkewedMegamorphic { .. } | ReceiverTypeResolution::Megamorphic => { if get_option!(stats) { - let reason = if has_block { SendMegamorphic } else { SendWithoutBlockMegamorphic }; - self.set_dynamic_send_reason(insn_id, reason); + self.set_dynamic_send_reason(insn_id, SendMegamorphic); } self.push_insn_id(block, insn_id); continue; } ReceiverTypeResolution::Polymorphic => { if get_option!(stats) { - let reason = if has_block { SendPolymorphic } else { SendWithoutBlockPolymorphic }; - self.set_dynamic_send_reason(insn_id, reason); + self.set_dynamic_send_reason(insn_id, SendPolymorphic); } self.push_insn_id(block, insn_id); continue; } ReceiverTypeResolution::NoProfile => { - let reason = if has_block { SendNoProfiles } else { SendWithoutBlockNoProfiles }; - self.set_dynamic_send_reason(insn_id, reason); + self.set_dynamic_send_reason(insn_id, SendNoProfiles); self.push_insn_id(block, insn_id); continue; } @@ -4115,8 +4100,7 @@ impl Function { // Do method lookup let mut cme = unsafe { rb_callable_method_entry(klass, mid) }; if cme.is_null() { - let reason = if has_block { SendNotOptimizedMethodType(MethodType::Null) } else { SendWithoutBlockNotOptimizedMethodType(MethodType::Null) }; - self.set_dynamic_send_reason(insn_id, reason); + self.set_dynamic_send_reason(insn_id, SendNotOptimizedMethodType(MethodType::Null)); self.push_insn_id(block, insn_id); continue; } // Load an overloaded cme if applicable. See vm_search_cc(). @@ -4128,8 +4112,7 @@ impl Function { (METHOD_VISI_PRIVATE, true) => {} (METHOD_VISI_PROTECTED, true) => {} _ => { - let reason = if has_block { SendNotOptimizedNeedPermission } else { SendWithoutBlockNotOptimizedNeedPermission }; - self.set_dynamic_send_reason(insn_id, reason); + self.set_dynamic_send_reason(insn_id, SendNotOptimizedNeedPermission); self.push_insn_id(block, insn_id); continue; } } @@ -4376,8 +4359,7 @@ impl Function { // Get the profiled type to check if the fields is embedded or heap allocated. let Some(is_embedded) = self.profiled_type_of_at(recv, state).map(|t| t.flags().is_struct_embedded()) else { // No (monomorphic/skewed polymorphic) profile info - let reason = if has_block { SendNoProfiles } else { SendWithoutBlockNoProfiles }; - self.set_dynamic_send_reason(insn_id, reason); + self.set_dynamic_send_reason(insn_id, SendNoProfiles); self.push_insn_id(block, insn_id); continue; }; // Check singleton class assumption first, before emitting other patchpoints @@ -4418,7 +4400,7 @@ impl Function { self.make_equal_to(insn_id, replacement); }, _ => { - self.set_dynamic_send_reason(insn_id, SendWithoutBlockNotOptimizedMethodTypeOptimized(OptimizedMethodType::from(opt_type))); + self.set_dynamic_send_reason(insn_id, SendNotOptimizedMethodTypeOptimized(OptimizedMethodType::from(opt_type))); self.push_insn_id(block, insn_id); continue; }, }; @@ -4639,8 +4621,7 @@ impl Function { self.push_insn_id(block, insn_id); } else { - let reason = if has_block { SendNotOptimizedMethodType(MethodType::from(def_type)) } else { SendWithoutBlockNotOptimizedMethodType(MethodType::from(def_type)) }; - self.set_dynamic_send_reason(insn_id, reason); + self.set_dynamic_send_reason(insn_id, SendNotOptimizedMethodType(MethodType::from(def_type))); self.push_insn_id(block, insn_id); continue; } } @@ -5682,7 +5663,7 @@ impl Function { assert!(self.blocks[block.0].insns.is_empty()); for insn_id in old_insns { match self.find(insn_id) { - Insn::Send { state, reason: SendFallbackReason::SendWithoutBlockNoProfiles | SendFallbackReason::SendNoProfiles, .. } => { + Insn::Send { state, reason: SendFallbackReason::SendNoProfiles, .. } => { self.push_insn(block, Insn::SideExit { state, reason: Box::new(SideExitReason::NoProfileSend), recompile: Some(Recompile) }); // SideExit is a terminator; don't add remaining instructions break; @@ -9170,7 +9151,7 @@ fn add_iseq_to_hir( fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![send] })); } // In the fallthrough case, do a generic interpreter send and then join. - let reason = SendWithoutBlockPolymorphicFallback; + let reason = SendPolymorphicFallback; let send = fun.push_insn(block, Insn::Send { recv, cd, block: None, args, state: exit_id, reason }); fun.push_insn(block, Insn::Jump(BranchEdge { target: join_block, args: vec![send] })); state.stack_push(join_param); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 7ef481ef08873b..9c8113faeb8ab5 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -1671,7 +1671,7 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - v11:BasicObject = Send v6, :foo # SendFallbackReason: SendWithoutBlock: unsupported method type Null + v11:BasicObject = Send v6, :foo # SendFallbackReason: Send: unsupported method type Null CheckInterrupts Return v11 "); @@ -3801,7 +3801,7 @@ mod hir_opt_tests { v42:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1048)) Jump bb8(v42, v52) bb8(v32:BasicObject, v33:BasicObject): - v47:BasicObject = Send v32, :call, v11, v13 # SendFallbackReason: SendWithoutBlock: unsupported optimized method type BlockCall + v47:BasicObject = Send v32, :call, v11, v13 # SendFallbackReason: Send: unsupported optimized method type BlockCall CheckInterrupts PopInlineFrame Return v47 @@ -8442,7 +8442,7 @@ mod hir_opt_tests { v60:TrueClass = Const Value(true) Jump bb7(v60) bb11(): - v48:BasicObject = Send v31, :! # SendFallbackReason: SendWithoutBlock: polymorphic fallback + v48:BasicObject = Send v31, :! # SendFallbackReason: Send: polymorphic fallback Jump bb7(v48) bb7(v35:BasicObject): CheckInterrupts @@ -9761,7 +9761,7 @@ mod hir_opt_tests { v31:BasicObject = GetIvar v19, :@foo Jump bb4(v31) bb6(): - v22:BasicObject = Send v10, :foo # SendFallbackReason: SendWithoutBlock: polymorphic fallback + v22:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback Jump bb4(v22) bb4(v15:BasicObject): CheckInterrupts @@ -10094,7 +10094,7 @@ mod hir_opt_tests { v54:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1048)) Jump bb13(v54, v27) bb13(v44:BasicObject, v45:BasicObject): - v57:BasicObject = Send v44, :call # SendFallbackReason: SendWithoutBlock: no profile data available + v57:BasicObject = Send v44, :call # SendFallbackReason: Send: no profile data available CheckInterrupts Jump bb4(v57) bb6(v62:ObjectSubclass[class_exact*:Object@VALUE(0x1000)], v63:BasicObject): @@ -15764,7 +15764,7 @@ mod hir_opt_tests { PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Obj) v12:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v14:BasicObject = Send v12, :secret # SendFallbackReason: SendWithoutBlock: method private or protected and no FCALL + v14:BasicObject = Send v12, :secret # SendFallbackReason: Send: method private or protected and no FCALL CheckInterrupts Return v14 "); @@ -15819,7 +15819,7 @@ mod hir_opt_tests { PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Obj) v12:BasicObjectExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v14:BasicObject = Send v12, :initialize # SendFallbackReason: SendWithoutBlock: method private or protected and no FCALL + v14:BasicObject = Send v12, :initialize # SendFallbackReason: Send: method private or protected and no FCALL CheckInterrupts Return v14 "); @@ -15847,7 +15847,7 @@ mod hir_opt_tests { PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Obj) v12:ObjectExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v14:BasicObject = Send v12, :toplevel_method # SendFallbackReason: SendWithoutBlock: method private or protected and no FCALL + v14:BasicObject = Send v12, :toplevel_method # SendFallbackReason: Send: method private or protected and no FCALL CheckInterrupts Return v14 "); @@ -15906,7 +15906,7 @@ mod hir_opt_tests { PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Obj) v12:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v14:BasicObject = Send v12, :secret # SendFallbackReason: SendWithoutBlock: method private or protected and no FCALL + v14:BasicObject = Send v12, :secret # SendFallbackReason: Send: method private or protected and no FCALL CheckInterrupts Return v14 "); @@ -16719,7 +16719,7 @@ mod hir_opt_tests { v45:Fixnum[4] = Const Value(4) Jump bb4(v45) bb8(): - v28:BasicObject = Send v10, :foo # SendFallbackReason: SendWithoutBlock: polymorphic fallback + v28:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback Jump bb4(v28) bb4(v15:BasicObject): v31:Fixnum[2] = Const Value(2) @@ -16772,7 +16772,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Integer@0x1040, itself@0x1010, cme:0x1018) Jump bb4(v25) bb8(): - v28:BasicObject = Send v10, :itself # SendFallbackReason: SendWithoutBlock: polymorphic fallback + v28:BasicObject = Send v10, :itself # SendFallbackReason: Send: polymorphic fallback Jump bb4(v28) bb4(v15:BasicObject): CheckInterrupts @@ -16828,7 +16828,7 @@ mod hir_opt_tests { v40:StringExact = CCallVariadic v25, :Integer#to_s@0x1040 Jump bb4(v40) bb8(): - v28:BasicObject = Send v10, :to_s # SendFallbackReason: SendWithoutBlock: polymorphic fallback + v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback Jump bb4(v28) bb4(v15:BasicObject): CheckInterrupts @@ -16881,7 +16881,7 @@ mod hir_opt_tests { v40:BasicObject = CCallWithFrame v25, :Float#to_s@0x1040 Jump bb4(v40) bb8(): - v28:BasicObject = Send v10, :to_s # SendFallbackReason: SendWithoutBlock: polymorphic fallback + v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback Jump bb4(v28) bb4(v15:BasicObject): CheckInterrupts @@ -16934,7 +16934,7 @@ mod hir_opt_tests { v38:StringExact = InvokeBuiltin leaf , v25 Jump bb4(v38) bb8(): - v28:BasicObject = Send v10, :to_s # SendFallbackReason: SendWithoutBlock: polymorphic fallback + v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback Jump bb4(v28) bb4(v15:BasicObject): CheckInterrupts @@ -16983,7 +16983,7 @@ mod hir_opt_tests { v31:Fixnum[3] = Const Value(3) Jump bb4(v31) bb6(): - v22:BasicObject = Send v10, :foo # SendFallbackReason: SendWithoutBlock: polymorphic fallback + v22:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback Jump bb4(v22) bb4(v15:BasicObject): CheckInterrupts @@ -17708,7 +17708,7 @@ mod hir_opt_tests { bb5(): v18:Truthy = RefineType v10, Truthy v22:Fixnum[42] = Const Value(42) - v24:BasicObject = Send v9, :greet_final, v22 # SendFallbackReason: SendWithoutBlock: no profile data available + v24:BasicObject = Send v9, :greet_final, v22 # SendFallbackReason: Send: no profile data available CheckInterrupts Return v24 bb4(v29:BasicObject, v30:Falsy): @@ -18501,7 +18501,7 @@ mod hir_opt_tests { v45:BasicObject = CCallWithFrame v30, :Float#*@0x1040, v13 Jump bb4(v45) bb8(): - v33:BasicObject = Send v12, :*, v13 # SendFallbackReason: SendWithoutBlock: polymorphic fallback + v33:BasicObject = Send v12, :*, v13 # SendFallbackReason: Send: polymorphic fallback Jump bb4(v33) bb4(v20:BasicObject): CheckInterrupts @@ -19995,7 +19995,7 @@ mod hir_opt_tests { v44:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1050)) Jump bb8(v44, v53) bb8(v34:BasicObject, v35:BasicObject): - v48:BasicObject = Send v34, :call, v10 # SendFallbackReason: SendWithoutBlock: unsupported optimized method type BlockCall + v48:BasicObject = Send v34, :call, v10 # SendFallbackReason: Send: unsupported optimized method type BlockCall CheckInterrupts PopInlineFrame PatchPoint NoEPEscape(test) diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 4c95e9f61d2f83..6a05e5f50788f9 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -251,22 +251,16 @@ make_counters! { // Send fallback counters that are summed as dynamic_send_count dynamic_send { // send_fallback_: Fallback reasons for send-ish instructions - send_fallback_send_without_block_polymorphic, - send_fallback_send_without_block_megamorphic, - send_fallback_send_without_block_no_profiles, - send_fallback_send_without_block_cfunc_not_variadic, - send_fallback_send_without_block_cfunc_array_variadic, - send_fallback_send_without_block_not_optimized_method_type, - send_fallback_send_without_block_not_optimized_method_type_optimized, - send_fallback_send_without_block_not_optimized_need_permission, + send_fallback_send_cfunc_not_variadic, + send_fallback_send_not_optimized_method_type_optimized, send_fallback_too_many_args_for_lir, - send_fallback_send_without_block_bop_redefined, - send_fallback_send_without_block_operands_not_fixnum, - send_fallback_send_without_block_polymorphic_fallback, - send_fallback_send_without_block_direct_keyword_mismatch, - send_fallback_send_without_block_direct_keyword_count_mismatch, - send_fallback_send_without_block_direct_missing_keyword, - send_fallback_send_without_block_direct_too_many_keywords, + send_fallback_send_bop_redefined, + send_fallback_send_operands_not_fixnum, + send_fallback_send_polymorphic_fallback, + send_fallback_send_direct_keyword_mismatch, + send_fallback_send_direct_keyword_count_mismatch, + send_fallback_send_direct_missing_keyword, + send_fallback_send_direct_too_many_keywords, send_fallback_send_polymorphic, send_fallback_send_megamorphic, send_fallback_send_no_profiles, @@ -384,27 +378,12 @@ make_counters! { // The number of times YARV instructions are executed on JIT code zjit_insn_count, - // Method call def_type related to send without block fallback to dynamic dispatch - unspecialized_send_without_block_def_type_iseq, - unspecialized_send_without_block_def_type_cfunc, - unspecialized_send_without_block_def_type_attrset, - unspecialized_send_without_block_def_type_ivar, - unspecialized_send_without_block_def_type_bmethod, - unspecialized_send_without_block_def_type_zsuper, - unspecialized_send_without_block_def_type_alias, - unspecialized_send_without_block_def_type_undef, - unspecialized_send_without_block_def_type_not_implemented, - unspecialized_send_without_block_def_type_optimized, - unspecialized_send_without_block_def_type_missing, - unspecialized_send_without_block_def_type_refined, - unspecialized_send_without_block_def_type_null, - // Method call optimized_type related to send without block fallback to dynamic dispatch - unspecialized_send_without_block_def_type_optimized_send, - unspecialized_send_without_block_def_type_optimized_call, - unspecialized_send_without_block_def_type_optimized_block_call, - unspecialized_send_without_block_def_type_optimized_struct_aref, - unspecialized_send_without_block_def_type_optimized_struct_aset, + unspecialized_send_def_type_optimized_send, + unspecialized_send_def_type_optimized_call, + unspecialized_send_def_type_optimized_block_call, + unspecialized_send_def_type_optimized_struct_aref, + unspecialized_send_def_type_optimized_struct_aset, // Method call def_type related to send fallback to dynamic dispatch unspecialized_send_def_type_iseq, @@ -686,24 +665,17 @@ pub fn send_fallback_counter(reason: crate::hir::SendFallbackReason) -> Counter use crate::hir::SendFallbackReason::*; use crate::stats::Counter::*; match reason { - SendWithoutBlockPolymorphic => send_fallback_send_without_block_polymorphic, - SendWithoutBlockMegamorphic => send_fallback_send_without_block_megamorphic, - SendWithoutBlockNoProfiles => send_fallback_send_without_block_no_profiles, - SendWithoutBlockCfuncNotVariadic => send_fallback_send_without_block_cfunc_not_variadic, - SendWithoutBlockCfuncArrayVariadic => send_fallback_send_without_block_cfunc_array_variadic, - SendWithoutBlockNotOptimizedMethodType(_) => send_fallback_send_without_block_not_optimized_method_type, - SendWithoutBlockNotOptimizedMethodTypeOptimized(_) - => send_fallback_send_without_block_not_optimized_method_type_optimized, - SendWithoutBlockNotOptimizedNeedPermission - => send_fallback_send_without_block_not_optimized_need_permission, + SendCfuncNotVariadic => send_fallback_send_cfunc_not_variadic, + SendNotOptimizedMethodTypeOptimized(_) + => send_fallback_send_not_optimized_method_type_optimized, TooManyArgsForLir => send_fallback_too_many_args_for_lir, - SendWithoutBlockBopRedefined => send_fallback_send_without_block_bop_redefined, - SendWithoutBlockOperandsNotFixnum => send_fallback_send_without_block_operands_not_fixnum, - SendWithoutBlockPolymorphicFallback => send_fallback_send_without_block_polymorphic_fallback, - SendDirectKeywordMismatch => send_fallback_send_without_block_direct_keyword_mismatch, - SendDirectKeywordCountMismatch => send_fallback_send_without_block_direct_keyword_count_mismatch, - SendDirectMissingKeyword => send_fallback_send_without_block_direct_missing_keyword, - SendDirectTooManyKeywords => send_fallback_send_without_block_direct_too_many_keywords, + SendBopRedefined => send_fallback_send_bop_redefined, + SendOperandsNotFixnum => send_fallback_send_operands_not_fixnum, + SendPolymorphicFallback => send_fallback_send_polymorphic_fallback, + SendDirectKeywordMismatch => send_fallback_send_direct_keyword_mismatch, + SendDirectKeywordCountMismatch => send_fallback_send_direct_keyword_count_mismatch, + SendDirectMissingKeyword => send_fallback_send_direct_missing_keyword, + SendDirectTooManyKeywords => send_fallback_send_direct_too_many_keywords, SendPolymorphic => send_fallback_send_polymorphic, SendMegamorphic => send_fallback_send_megamorphic, SendNoProfiles => send_fallback_send_no_profiles, @@ -736,37 +708,16 @@ pub fn send_fallback_counter(reason: crate::hir::SendFallbackReason) -> Counter } } -pub fn send_without_block_fallback_counter_for_method_type(method_type: crate::hir::MethodType) -> Counter { - use crate::hir::MethodType::*; - use crate::stats::Counter::*; - - match method_type { - Iseq => unspecialized_send_without_block_def_type_iseq, - Cfunc => unspecialized_send_without_block_def_type_cfunc, - Attrset => unspecialized_send_without_block_def_type_attrset, - Ivar => unspecialized_send_without_block_def_type_ivar, - Bmethod => unspecialized_send_without_block_def_type_bmethod, - Zsuper => unspecialized_send_without_block_def_type_zsuper, - Alias => unspecialized_send_without_block_def_type_alias, - Undefined => unspecialized_send_without_block_def_type_undef, - NotImplemented => unspecialized_send_without_block_def_type_not_implemented, - Optimized => unspecialized_send_without_block_def_type_optimized, - Missing => unspecialized_send_without_block_def_type_missing, - Refined => unspecialized_send_without_block_def_type_refined, - Null => unspecialized_send_without_block_def_type_null, - } -} - -pub fn send_without_block_fallback_counter_for_optimized_method_type(method_type: crate::hir::OptimizedMethodType) -> Counter { +pub fn send_fallback_counter_for_optimized_method_type(method_type: crate::hir::OptimizedMethodType) -> Counter { use crate::hir::OptimizedMethodType::*; use crate::stats::Counter::*; match method_type { - Send => unspecialized_send_without_block_def_type_optimized_send, - Call => unspecialized_send_without_block_def_type_optimized_call, - BlockCall => unspecialized_send_without_block_def_type_optimized_block_call, - StructAref => unspecialized_send_without_block_def_type_optimized_struct_aref, - StructAset => unspecialized_send_without_block_def_type_optimized_struct_aset, + Send => unspecialized_send_def_type_optimized_send, + Call => unspecialized_send_def_type_optimized_call, + BlockCall => unspecialized_send_def_type_optimized_block_call, + StructAref => unspecialized_send_def_type_optimized_struct_aref, + StructAset => unspecialized_send_def_type_optimized_struct_aset, } } From 1790ac443c3fd8d192547a5f8de93684c3ea8a21 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 15 Jul 2026 13:18:46 -0400 Subject: [PATCH 16/39] [ruby/prism] Fix up buffer treatment of stream parsing A couple of things to fix, notably that we relied on the stream returning strings and relied on it not lying about how much was being written. Now we no longer trust either and we verify instead. https://github.com/ruby/prism/commit/70147e5b44 Co-authored-by: Sutou Kouhei --- lib/prism/ffi.rb | 16 +++++- prism/extension.c | 46 +++++++++++++++-- prism/source.c | 61 +++++++++++++++------- prism/source.h | 7 +++ test/prism/api/parse_stream_test.rb | 78 +++++++++++++++++++++++++++++ test/prism/newline_test.rb | 1 + 6 files changed, 186 insertions(+), 23 deletions(-) diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb index 6b9bde51eaca49..01c1f4ace4971a 100644 --- a/lib/prism/ffi.rb +++ b/lib/prism/ffi.rb @@ -294,11 +294,23 @@ def parse_file(filepath, **options) # Mirror the Prism.parse_stream API by using the serialization API. def parse_stream(stream, **options) LibRubyParser::PrismBuffer.with do |buffer| + # The largest number of bytes a single character can occupy in any + # encoding Ruby supports. IO#gets(limit) may return up to + # (max_enc_len - 1) bytes more than requested to avoid splitting a + # multi-byte character, so we reserve that much headroom (plus the NUL + # terminator) to guarantee the result fits in the caller's buffer. This + # mirrors MAX_ENC_LEN in ext/prism/extension.c. + max_enc_len = 6 + source = +"" callback = -> (string, size, _) { - raise "Expected size to be >= 0, got: #{size}" if size <= 0 + raise "Expected size to be > #{max_enc_len}, got: #{size}" if size <= max_enc_len - if !(line = stream.gets(size - 1)).nil? + line = String.try_convert(stream.gets(size - max_enc_len)) + if !line.nil? + # A misbehaving `gets` may ignore the limit; never write past the + # buffer (one byte is reserved for the NUL terminator). + line = line.byteslice(0, size - 1) if line.bytesize > size - 1 source << line string.write_string("#{line}\x00", line.bytesize + 1) end diff --git a/prism/extension.c b/prism/extension.c index 27df8dac50ddff..bd6a7d22c3d08e 100644 --- a/prism/extension.c +++ b/prism/extension.c @@ -1067,13 +1067,40 @@ parse_stream_eof(void *stream) { } /** - * An implementation of fgets that is suitable for use with Ruby IO objects. + * The largest number of bytes a single character can occupy in any encoding + * that Ruby supports (CESU-8). `IO#gets(limit)` treats its argument as a soft + * limit: to avoid splitting a multi-byte character it may return up to + * `MAX_ENC_LEN - 1` more bytes than requested. Reserving `MAX_ENC_LEN` bytes of + * headroom therefore leaves room for both that overshoot and the terminating + * NUL byte. + * + * The value can be re-derived from the Ruby source tree with: + * + * grep -Erh "max (enc|byte) length" enc | grep -Eo '[0-9]+' | sort | tail -n1 + */ +#define MAX_ENC_LEN 6 + +/** + * An implementation of fgets that is suitable for use with Ruby IO objects. As + * required by pm_source_stream_fgets_t, this never writes more than `size` + * bytes into `string` (including the terminating NUL byte). */ static char * parse_stream_fgets(char *string, int size, void *stream) { - RUBY_ASSERT(size > 0); + RUBY_ASSERT(size > MAX_ENC_LEN); + + /* + * Request fewer bytes than the buffer can hold. `gets` may return more + * bytes than requested when the limit falls in the middle of a multi-byte + * character, and the reserved headroom guarantees the result still fits. + */ + VALUE line = rb_funcall((VALUE) stream, rb_intern("gets"), 1, INT2FIX(size - MAX_ENC_LEN)); - VALUE line = rb_funcall((VALUE) stream, rb_intern("gets"), 1, INT2FIX(size - 1)); + /* + * A well-behaved stream returns a String, or nil at EOF. Coerce anything + * else to nil so that we never treat a non-String as a byte buffer. + */ + line = rb_check_string_type(line); if (NIL_P(line)) { return NULL; } @@ -1081,12 +1108,23 @@ parse_stream_fgets(char *string, int size, void *stream) { const char *cstr = RSTRING_PTR(line); long length = RSTRING_LEN(line); - memcpy(string, cstr, length); + /* + * Defensively clamp the copy. A misbehaving `gets` may ignore the limit + * entirely and return an arbitrarily long string; we must never write past + * the caller's buffer. One byte is reserved for the NUL terminator. + */ + if (length > (long) (size - 1)) { + length = (long) (size - 1); + } + + memcpy(string, cstr, (size_t) length); string[length] = '\0'; return string; } +#undef MAX_ENC_LEN + /** * :markup: markdown * call-seq: diff --git a/prism/source.c b/prism/source.c index f61cb19c1bca2b..5a6982c3101405 100644 --- a/prism/source.c +++ b/prism/source.c @@ -368,25 +368,50 @@ pm_source_stream_read(pm_source_t *source) { #define LINE_SIZE 4096 char line[LINE_SIZE]; - while (memset(line, '\n', LINE_SIZE), source->stream.fgets(line, LINE_SIZE, source->stream.stream) != NULL) { + /* + * A chunk read from the stream may legitimately contain embedded NUL bytes, + * so the NUL terminator written by fgets cannot be located with strlen. + * Instead we prefill the buffer with a non-NUL sentinel before each read; + * the terminator is then the only NUL at or after the data, so it can be + * found by scanning from the end. The value only needs to differ from '\0'. + */ +#define LINE_FILL 0xff + + while (memset(line, LINE_FILL, LINE_SIZE), source->stream.fgets(line, LINE_SIZE, source->stream.stream) != NULL) { + /* + * Locate the NUL terminator written by fgets (the last NUL in the + * buffer) to recover the length of the chunk, then drop the terminator. + */ size_t length = LINE_SIZE; - while (length > 0 && line[length - 1] == '\n') length--; - - if (length == LINE_SIZE) { - /* - * If we read a line that is the maximum size and it doesn't end - * with a newline, then we'll just append it to the buffer and - * continue reading. - */ - length--; - pm_buffer_append_string(buffer, line, length); - continue; + while (length > 0 && line[length - 1] != '\0') length--; + if (length > 0) length--; + + /* + * An empty chunk means the stream returned an empty string instead of + * signaling EOF (a well-behaved stream never does this). We can't make + * progress on it, so stop reading rather than spinning forever. + */ + if (length == 0) { + break; } - /* Append the line to the buffer. */ - length--; + /* Append the chunk to the buffer. */ pm_buffer_append_string(buffer, line, length); + bool newline = (line[length - 1] == '\n'); + + /* + * A chunk with no trailing newline is either a line longer than the + * read buffer (keep reading the rest of it) or the final line at EOF. + * This is the only place the stream's EOF state changes what we do + * next, so it is the only place we ask the stream whether it has hit + * EOF. A newline-terminated line needs no such check: if it happens to + * be the last line, the next fgets returns NULL and ends the loop. + */ + if (!newline && !source->stream.feof(source->stream.stream)) { + continue; + } + /* * Check if the line matches the __END__ marker. If it does, then stop * reading and return false. In most circumstances, this means we should @@ -418,14 +443,16 @@ pm_source_stream_read(pm_source_t *source) { } /* - * All data should be read via gets. If the string returned by gets - * _doesn't_ end with a newline, then we assume we hit EOF condition. + * A chunk that reached here without a trailing newline is the final + * line at EOF (the check above would have continued otherwise), so + * there is nothing more to read. */ - if (source->stream.feof(source->stream.stream)) { + if (!newline) { break; } } +#undef LINE_FILL #undef LINE_SIZE source->stream.eof = true; diff --git a/prism/source.h b/prism/source.h index c79987d3fb9c34..115914392f8ec3 100644 --- a/prism/source.h +++ b/prism/source.h @@ -24,6 +24,13 @@ typedef struct pm_source_t pm_source_t; * This function is used to retrieve a line of input from a stream. It closely * mirrors that of fgets so that fgets can be used as the default * implementation. + * + * As with fgets, implementations MUST write at most `size` bytes into + * `string`, including the terminating NUL byte (i.e. at most `size - 1` bytes + * of data followed by a '\0'). The caller relies on this bound for memory + * safety, so an implementation that reads from a source which may return more + * data than requested (for example a multi-byte-aware `gets`) is responsible + * for clamping the amount it copies. Returns `string`, or NULL on EOF. */ typedef char * (pm_source_stream_fgets_t)(char *string, int size, void *stream); diff --git a/test/prism/api/parse_stream_test.rb b/test/prism/api/parse_stream_test.rb index 3bc86fbd6167b6..ffbedc915d1491 100644 --- a/test/prism/api/parse_stream_test.rb +++ b/test/prism/api/parse_stream_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "../test_helper" +require "timeout" module Prism class ParseStreamTest < TestCase @@ -43,6 +44,18 @@ def test___END__ assert_equal "5 + 6\n", io.read end + # __END__ as the final line with no trailing newline must still be detected + # as the terminator. This exercises the EOF check for a chunk that does not + # end in a newline, distinct from a line longer than the internal read + # buffer (which also has no trailing newline but is not the end of input). + def test___END___without_trailing_newline + io = StringIO.new("1 + 2\n__END__") + result = Prism.parse_stream(io) + + assert result.success? + assert_equal 1, result.value.statements.body.length + end + def test_false___END___in_string io = StringIO.new(<<~RUBY) 1 + 2 @@ -114,5 +127,70 @@ def test_nul_bytes assert result.success? assert_equal 3, result.value.statements.body.length end + + # `IO#gets(limit)` will not split a multi-byte character, so it can return + # more bytes than requested when the limit falls in the middle of one. When + # that character straddles the internal read buffer boundary, the old code + # wrote past the buffer and dropped the overflowing bytes, corrupting the + # content (and overflowing the stack). Sweep offsets around the 4096-byte + # boundary with both 3- and 4-byte characters so the straddle is hit + # regardless of the exact internal read size. + def test_multibyte_on_read_boundary + ["あ", "\u{1F600}"].each do |char| + (4080..4100).each do |prefix| + body = ("a" * prefix) + char + result = Prism.parse_stream(StringIO.new("\"#{body}\"")) + + assert result.success?, "parse failed at prefix=#{prefix} char=#{char.dump}" + assert_equal body, result.value.statements.body[0].content, "content mismatch at prefix=#{prefix} char=#{char.dump}" + end + end + end + + # A misbehaving stream whose `gets` ignores its limit and returns an + # arbitrarily long string must not overflow the internal buffer. The old + # code copied the full returned length into a fixed 4096-byte buffer. + def test_gets_exceeding_limit + stream = Object.new + def stream.gets(limit = nil) + return nil if defined?(@done) && @done + @done = true + ("x" * 100_000) + "\n" + end + def stream.eof?; defined?(@done) && @done; end + + result = Prism.parse_stream(stream) + assert result.success? + end + + # A misbehaving stream whose `gets` returns a non-String must not be read as + # a byte buffer. The old code called RSTRING_PTR on whatever was returned. + def test_gets_returning_non_string + stream = Object.new + def stream.gets(limit = nil) + return nil if defined?(@done) && @done + @done = true + 1234 + end + def stream.eof?; defined?(@done) && @done; end + + assert_nothing_raised do + Prism.parse_stream(stream) + end + end + + # A misbehaving stream whose `gets` returns an empty string instead of nil + # while never reporting EOF must not loop forever. A well-behaved stream + # returns nil at EOF, so an empty chunk is treated as the end of input. + def test_gets_returning_empty_string + stream = Object.new + def stream.gets(limit = nil); ""; end + def stream.eof?; false; end + + result = Timeout.timeout(10) { Prism.parse_stream(stream) } + assert result.success? + rescue Timeout::Error + flunk "Prism.parse_stream looped forever on a stream returning empty strings" + end end end diff --git a/test/prism/newline_test.rb b/test/prism/newline_test.rb index 97e698202df748..ffaadf952c9d0f 100644 --- a/test/prism/newline_test.rb +++ b/test/prism/newline_test.rb @@ -12,6 +12,7 @@ class NewlineTest < TestCase regexp_test.rb test_helper.rb unescape_test.rb + api/parse_stream_test.rb encoding/regular_expression_encoding_test.rb encoding/string_encoding_test.rb result/breadth_first_search_test.rb From d7bdc2a33f1589cd8b70d169a2bbdf0525067d8a Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 16 Jul 2026 01:28:40 +0900 Subject: [PATCH 17/39] Windows: Skip csv tests for ractor which look hanging up --- tool/test-bundled-gems.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tool/test-bundled-gems.rb b/tool/test-bundled-gems.rb index 618ce4b909aa0f..5712173755f974 100644 --- a/tool/test-bundled-gems.rb +++ b/tool/test-bundled-gems.rb @@ -128,6 +128,8 @@ when "csv" first_timeout = 30 + test_command = [ruby, *run_opts, "-C", "#{gem_dir}/src/#{gem}", "run-test.rb"] + test_command << "--ignore-name=/ractor/" if /mswin|mingw/ =~ RUBY_PLATFORM when "win32ole" next unless /mswin|mingw/ =~ RUBY_PLATFORM From 2619d3af01acbbc7c7c28351dfe363a73edbe2bf Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 16 Jul 2026 01:49:39 +0900 Subject: [PATCH 18/39] Windows: Skip debug tests using pty --- tool/test-bundled-gems.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tool/test-bundled-gems.rb b/tool/test-bundled-gems.rb index 5712173755f974..9e5ca186ff456f 100644 --- a/tool/test-bundled-gems.rb +++ b/tool/test-bundled-gems.rb @@ -116,6 +116,9 @@ first_timeout *= 3 when "debug" + # needs pty + next unless /mswin|mingw/ =~ RUBY_PLATFORM + # Since debug gem requires debug.so in child processes without # activating the gem, we preset necessary paths in RUBYLIB # environment variable. From 6e842889cece3c590c5d861b8cf5974b40aa7360 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 16 Jul 2026 01:30:04 +0900 Subject: [PATCH 19/39] Start heavy tests first --- tool/test-bundled-gems.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tool/test-bundled-gems.rb b/tool/test-bundled-gems.rb index 9e5ca186ff456f..a8fbae2b793c9d 100644 --- a/tool/test-bundled-gems.rb +++ b/tool/test-bundled-gems.rb @@ -157,6 +157,10 @@ end end +heavy_tests = %w[rbs debug reline win32ole irb drb net-imap rdoc typeprof racc rake] +others = heavy_tests.size +jobs.sort_by! {|job| heavy_tests.index(job[:gem]) || (others += 1)} + results = Array.new(jobs.size) queue = Queue.new jobs.each_with_index { |j, i| queue << [j, i] } From efb3a7344589c64ef2438beaa39b8d0e8b67cdc5 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 15 Jul 2026 19:30:41 +0200 Subject: [PATCH 20/39] rb_imemo_fields_ptr: assume embedded --- shape.h | 7 +------ variable.c | 2 ++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/shape.h b/shape.h index e4423f9327370c..20cd92c77bba7f 100644 --- a/shape.h +++ b/shape.h @@ -412,12 +412,7 @@ rb_imemo_fields_ptr(VALUE fields_obj) return NULL; } - if (UNLIKELY(rb_obj_shape_extended_p(fields_obj))) { - RUBY_ASSERT(RB_TYPE_P(fields_obj, T_OBJECT)); - fields_obj = ROBJECT(fields_obj)->as.extended; - RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); - } - + RUBY_ASSERT(rb_obj_shape_embedded_p(fields_obj)); return IMEMO_OBJ_FIELDS(fields_obj)->as.embed.fields; } diff --git a/variable.c b/variable.c index a73bb8826382cf..fc40cf3794adc7 100644 --- a/variable.c +++ b/variable.c @@ -2154,7 +2154,9 @@ obj_fields_each(VALUE obj, rb_ivar_foreach_callback_func *func, st_data_t arg, b .ivar_only = ivar_only, }; + VALUE fields_obj = ROBJECT_FIELDS_OBJ(obj); shape_id_t shape_id = RBASIC_SHAPE_ID(obj); + if (rb_shape_complex_p(shape_id)) { st_foreach_safe(ROBJECT_FIELDS_HASH(obj), each_hash_iv, (st_data_t)&itr_data); } From 0b681246d6bb9fd33d4a00d58546a608131936ea Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 15 Jul 2026 19:35:37 +0200 Subject: [PATCH 21/39] Get rid of ROBJECT_FIELDS_COUNT It's basrely used. --- ext/objspace/objspace_dump.c | 5 ++++- ractor.c | 4 ++-- shape.h | 25 ------------------------- 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/ext/objspace/objspace_dump.c b/ext/objspace/objspace_dump.c index fb25c400bd4ac3..83cfd9befb6696 100644 --- a/ext/objspace/objspace_dump.c +++ b/ext/objspace/objspace_dump.c @@ -597,10 +597,13 @@ dump_object(VALUE obj, struct dump_config *dc) } dump_append(dc, ", \"ivars\":"); - dump_append_lu(dc, ROBJECT_FIELDS_COUNT(obj)); if (rb_obj_shape_complex_p(obj)) { + dump_append_lu(dc, rb_st_table_size(ROBJECT_FIELDS_HASH(obj))); dump_append(dc, ", \"complex_shape\":true"); } + else { + dump_append_lu(dc, RSHAPE_LEN(RBASIC_SHAPE_ID(obj))); + } break; case T_FILE: diff --git a/ractor.c b/ractor.c index e58a85d100ea7d..c1384c03a90967 100644 --- a/ractor.c +++ b/ractor.c @@ -1896,10 +1896,10 @@ obj_traverse_replace_i(VALUE obj, struct obj_traverse_replace_data *data) if (d.stop) return 1; } else { - uint32_t len = ROBJECT_FIELDS_COUNT_NOT_COMPLEX(obj); + attr_index_t len = RSHAPE_LEN(RBASIC_SHAPE_ID(obj)); VALUE *ptr = ROBJECT_FIELDS(obj); - for (uint32_t i = 0; i < len; i++) { + for (attr_index_t i = 0; i < len; i++) { CHECK_AND_REPLACE(obj, ptr[i]); } } diff --git a/shape.h b/shape.h index 20cd92c77bba7f..3abb1f26cf4aa7 100644 --- a/shape.h +++ b/shape.h @@ -714,31 +714,6 @@ ROBJECT_FIELDS_HASH(VALUE obj) return rb_imemo_fields_complex_tbl(ROBJECT(obj)->as.extended); } -static inline uint32_t -ROBJECT_FIELDS_COUNT_COMPLEX(VALUE obj) -{ - return (uint32_t)rb_st_table_size(ROBJECT_FIELDS_HASH(obj)); -} - -static inline uint32_t -ROBJECT_FIELDS_COUNT_NOT_COMPLEX(VALUE obj) -{ - RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); - RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - return RSHAPE(RBASIC_SHAPE_ID(obj))->next_field_index; -} - -static inline uint32_t -ROBJECT_FIELDS_COUNT(VALUE obj) -{ - if (rb_obj_shape_complex_p(obj)) { - return ROBJECT_FIELDS_COUNT_COMPLEX(obj); - } - else { - return ROBJECT_FIELDS_COUNT_NOT_COMPLEX(obj); - } -} - static inline VALUE ROBJECT_FIELDS_OBJ(VALUE obj) { From 0afd762b9ebf55b3b54d0bbc0eac96d37f60f859 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 15 Jul 2026 19:40:25 +0200 Subject: [PATCH 22/39] Get rid of ROBJECT_FIELDS_CAPACITY It's barely used. --- gc.c | 14 +++++++++----- shape.h | 10 ---------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/gc.c b/gc.c index 69f3345af82729..1b4a55fd9c5e41 100644 --- a/gc.c +++ b/gc.c @@ -1137,8 +1137,11 @@ rb_class_allocate_instance(VALUE klass) #if RUBY_DEBUG VALUE *ptr = ROBJECT_FIELDS(obj); - size_t fields_count = RSHAPE_LEN(RBASIC_SHAPE_ID(obj)); - for (size_t i = fields_count; i < ROBJECT_FIELDS_CAPACITY(obj); i++) { + shape_id_t shape_id = RBASIC_SHAPE_ID(obj); + attr_index_t fields_count = RSHAPE_LEN(shape_id); + attr_index_t capacity = RSHAPE_CAPACITY(shape_id); + + for (attr_index_t i = fields_count; i < capacity; i++) { ptr[i] = Qundef; } #endif @@ -3378,7 +3381,7 @@ rb_gc_obj_optimal_size(VALUE obj) return sizeof(struct RObject); } else { - size_t size = rb_obj_embedded_size(ROBJECT_FIELDS_CAPACITY(obj)); + size_t size = rb_obj_embedded_size(RSHAPE_CAPACITY(RBASIC_SHAPE_ID(obj))); if (rb_gc_size_allocatable_p(size)) { return size; } @@ -4876,12 +4879,13 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU switch (imemo_type(obj)) { case imemo_fields: { - if (rb_obj_shape_complex_p(obj)) { + shape_id_t shape_id = RBASIC_SHAPE_ID(obj); + if (rb_shape_complex_p(shape_id)) { size_t hash_len = rb_st_table_size(rb_imemo_fields_complex_tbl(obj)); APPEND_F("(complex) len:%zu", hash_len); } else { - APPEND_F("(embed) len:%d capa:%d", RSHAPE_LEN(RBASIC_SHAPE_ID(obj)), ROBJECT_FIELDS_CAPACITY(obj)); + APPEND_F("(embed) len:%d capa:%d", RSHAPE_LEN(shape_id), RSHAPE_CAPACITY(shape_id)); } APPEND_S("owner -> "); diff --git a/shape.h b/shape.h index 3abb1f26cf4aa7..e0af96ebe6394f 100644 --- a/shape.h +++ b/shape.h @@ -694,16 +694,6 @@ rb_imemo_fields_complex_tbl(VALUE fields_obj) return &IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table; } -static inline uint32_t -ROBJECT_FIELDS_CAPACITY(VALUE obj) -{ - RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); - // Asking for capacity doesn't make sense when the object is using - // a hash table for storing instance variables - RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - return RSHAPE_CAPACITY(RBASIC_SHAPE_ID(obj)); -} - static inline st_table * ROBJECT_FIELDS_HASH(VALUE obj) { From 74bd057f9cfc0503c205805979367248208200c0 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 15 Jul 2026 19:51:01 +0200 Subject: [PATCH 23/39] Get rid of ROBJECT_FIELDS_HASH --- ext/objspace/objspace_dump.c | 13 ++++++++----- ractor.c | 10 ++++++---- shape.h | 10 ---------- variable.c | 33 +++++++-------------------------- 4 files changed, 21 insertions(+), 45 deletions(-) diff --git a/ext/objspace/objspace_dump.c b/ext/objspace/objspace_dump.c index 83cfd9befb6696..e7778d08dd6214 100644 --- a/ext/objspace/objspace_dump.c +++ b/ext/objspace/objspace_dump.c @@ -591,20 +591,23 @@ dump_object(VALUE obj, struct dump_config *dc) dump_append(dc, "\""); break; - case T_OBJECT: - if (rb_obj_shape_embedded_p(obj)) { + case T_OBJECT: { + shape_id_t shape_id = RBASIC_SHAPE_ID(obj); + + if (rb_shape_embedded_p(shape_id)) { dump_append(dc, ", \"embedded\":true"); } dump_append(dc, ", \"ivars\":"); - if (rb_obj_shape_complex_p(obj)) { - dump_append_lu(dc, rb_st_table_size(ROBJECT_FIELDS_HASH(obj))); + if (rb_shape_complex_p(shape_id)) { + dump_append_lu(dc, rb_st_table_size(rb_imemo_fields_complex_tbl(ROBJECT_FIELDS_OBJ(obj)))); dump_append(dc, ", \"complex_shape\":true"); } else { - dump_append_lu(dc, RSHAPE_LEN(RBASIC_SHAPE_ID(obj))); + dump_append_lu(dc, RSHAPE_LEN(shape_id)); } break; + } case T_FILE: fptr = RFILE(obj)->fptr; diff --git a/ractor.c b/ractor.c index c1384c03a90967..40a2350f571786 100644 --- a/ractor.c +++ b/ractor.c @@ -1881,14 +1881,16 @@ obj_traverse_replace_i(VALUE obj, struct obj_traverse_replace_data *data) case T_OBJECT: { - if (rb_obj_shape_complex_p(obj)) { + VALUE fields_obj = ROBJECT_FIELDS_OBJ(obj); + shape_id_t shape_id = RBASIC_SHAPE_ID(fields_obj); + if (rb_shape_complex_p(shape_id)) { struct obj_traverse_replace_callback_data d = { .stop = false, .data = data, .src = obj, }; rb_st_foreach_with_replace( - ROBJECT_FIELDS_HASH(obj), + rb_imemo_fields_complex_tbl(fields_obj), obj_iv_hash_traverse_replace_foreach_i, obj_iv_hash_traverse_replace_i, (st_data_t)&d @@ -1896,8 +1898,8 @@ obj_traverse_replace_i(VALUE obj, struct obj_traverse_replace_data *data) if (d.stop) return 1; } else { - attr_index_t len = RSHAPE_LEN(RBASIC_SHAPE_ID(obj)); - VALUE *ptr = ROBJECT_FIELDS(obj); + attr_index_t len = RSHAPE_LEN(shape_id); + VALUE *ptr = rb_imemo_fields_ptr(fields_obj); for (attr_index_t i = 0; i < len; i++) { CHECK_AND_REPLACE(obj, ptr[i]); diff --git a/shape.h b/shape.h index e0af96ebe6394f..5a7dc76c4d12cd 100644 --- a/shape.h +++ b/shape.h @@ -694,16 +694,6 @@ rb_imemo_fields_complex_tbl(VALUE fields_obj) return &IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table; } -static inline st_table * -ROBJECT_FIELDS_HASH(VALUE obj) -{ - RBIMPL_ASSERT_TYPE(obj, RUBY_T_OBJECT); - RUBY_ASSERT(rb_obj_shape_complex_p(obj)); - RUBY_ASSERT(rb_obj_shape_extended_p(obj)); - - return rb_imemo_fields_complex_tbl(ROBJECT(obj)->as.extended); -} - static inline VALUE ROBJECT_FIELDS_OBJ(VALUE obj) { diff --git a/variable.c b/variable.c index fc40cf3794adc7..2f4e4a387faa3a 100644 --- a/variable.c +++ b/variable.c @@ -2019,32 +2019,12 @@ rb_obj_field_set(VALUE obj, shape_id_t target_shape_id, ID field_name, VALUE val static VALUE ivar_defined0(VALUE obj, ID id) { - attr_index_t index; - if (rb_obj_shape_complex_p(obj)) { - VALUE idx; - st_table *table = NULL; - switch (BUILTIN_TYPE(obj)) { - case T_CLASS: - case T_MODULE: - rb_bug("Unreachable"); - break; - - case T_IMEMO: - RUBY_ASSERT(IMEMO_TYPE_P(obj, imemo_fields)); - table = rb_imemo_fields_complex_tbl(obj); - break; - - case T_OBJECT: - table = ROBJECT_FIELDS_HASH(obj); - break; - - default: { - VALUE fields_obj = rb_obj_fields_no_ractor_check(obj); // defined? doesn't require ractor checks - table = rb_imemo_fields_complex_tbl(fields_obj); - } - } + // defined? doesn't require ractor checks + VALUE fields_obj = rb_obj_fields_no_ractor_check(obj); + st_table *table = rb_imemo_fields_complex_tbl(fields_obj); + VALUE idx; if (!table || !rb_st_lookup(table, id, &idx)) { return Qfalse; } @@ -2052,6 +2032,7 @@ ivar_defined0(VALUE obj, ID id) return Qtrue; } else { + attr_index_t index; return RBOOL(rb_shape_get_iv_index(RBASIC_SHAPE_ID(obj), id, &index)); } } @@ -2158,10 +2139,10 @@ obj_fields_each(VALUE obj, rb_ivar_foreach_callback_func *func, st_data_t arg, b shape_id_t shape_id = RBASIC_SHAPE_ID(obj); if (rb_shape_complex_p(shape_id)) { - st_foreach_safe(ROBJECT_FIELDS_HASH(obj), each_hash_iv, (st_data_t)&itr_data); + st_foreach_safe(rb_imemo_fields_complex_tbl(fields_obj), each_hash_iv, (st_data_t)&itr_data); } else { - itr_data.fields = ROBJECT_FIELDS(obj); + itr_data.fields = rb_imemo_fields_ptr(fields_obj); itr_data.shape_id = shape_id; iterate_over_shapes(shape_id, func, &itr_data); } From 0abe2447e1cc1c86a96b298a4d5d2776f01fe16f Mon Sep 17 00:00:00 2001 From: Hartley McGuire Date: Wed, 15 Jul 2026 14:25:42 -0400 Subject: [PATCH 24/39] Make Time.at w/ in: 1.5x faster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` $ ips --ruby 'ruby-dev --yjit' --ruby 'ruby-master --yjit' -e 'Time.at(1_700_000_000, in: 0)' ruby 4.1.0dev (2026-07-15T17:09:20Z master 6cd755deab) +YJIT +PRISM [arm64-darwin25] Time.at(1_700_000_000, in: 0): 8.920M i/s (± 2.0%, GC 3.3%) ruby 4.1.0dev (2026-07-15T17:09:20Z master 6cd755deab) +YJIT +PRISM [arm64-darwin25] Time.at(1_700_000_000, in: 0): 5.894M i/s (± 0.5%, GC 4.2%) Summary ruby ruby-dev --yjit ran 1.51 ± 0.03 times faster than ruby ruby-master --yjit ``` Other callers of zone_localtime use maybe_tzobj_p to short circuit the utc_to_local respond_to?/call, but this one didn't. --- time.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time.c b/time.c index d6826d7aad9ac8..b518d194933df6 100644 --- a/time.c +++ b/time.c @@ -4140,7 +4140,7 @@ static VALUE time_zonelocal(VALUE time, VALUE off) { VALUE zone = off; - if (zone_localtime(zone, time)) return time; + if (maybe_tzobj_p(zone) && zone_localtime(zone, time)) return time; if (NIL_P(off = utc_offset_arg(off))) { off = zone; From a758990d44231018213e399d80a79192cc9132d4 Mon Sep 17 00:00:00 2001 From: Hartley McGuire Date: Wed, 15 Jul 2026 14:56:46 -0400 Subject: [PATCH 25/39] Make Time.at w/ in: another 1.2x faster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` $ ips --ruby 'ruby-maybe --yjit' --ruby 'ruby-dev --yjit' -e 'Time.at(1_700_000_000, in: 0)' ruby 4.1.0dev (2026-07-15T17:09:20Z master 6cd755deab) +YJIT +PRISM [arm64-darwin25] Time.at(1_700_000_000, in: 0): 8.944M i/s (± 0.7%, GC 3.2%) ruby 4.1.0dev (2026-07-15T17:09:20Z master 6cd755deab) +YJIT +PRISM [arm64-darwin25] Time.at(1_700_000_000, in: 0): 10.925M i/s (± 0.9%, GC 4.0%) Summary ruby ruby-dev --yjit ran 1.22 ± 0.01 times faster than ruby ruby-maybe --yjit ``` utc_offset_arg checks whether its argument can be to_str'd, which Integer's (should) not be, so we can fast path it to speed it up --- time.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/time.c b/time.c index b518d194933df6..d9afd3fd18ccb0 100644 --- a/time.c +++ b/time.c @@ -2233,6 +2233,10 @@ invalid_utc_offset(VALUE zone) static VALUE utc_offset_arg(VALUE arg) { + if (RB_INTEGER_TYPE_P(arg)) { + return arg; + } + VALUE tmp; if (!NIL_P(tmp = rb_check_string_type(arg))) { int n = 0; From f7eca0d41e6e4dfbf0828e724ea09b97773ef4f3 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 9 Jul 2026 13:31:22 -0700 Subject: [PATCH 26/39] Batch cleanup geniv before sweep Previously we needed to check every object to determine whether or not it might have an entry in the geniv table. This commit adds a function, rb_gc_vm_weak_table_essential_p, which allows gc.c to decide which tables to always handle in batch. MMTK will continue to call rb_gc_vm_weak_table_foreach on all tables. This only changes the default GC to sweep the "essential" tables first, but continue to call rb_gc_obj_free_vm_weak_references on other objects. This makes the geniv table "essental", as it's usually small and needs to be tested on every type of object. It leaves the fstring table, symbol table, callinfo table, and overloaded CME table as-is because those tables are (mostly) larger and are only needed by specific object types. --- gc.c | 23 ++++++++++++++--------- gc/default/default.c | 20 ++++++++++++++++++++ gc/gc.h | 1 + 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/gc.c b/gc.c index 1b4a55fd9c5e41..e03d1362b84eb2 100644 --- a/gc.c +++ b/gc.c @@ -1411,11 +1411,6 @@ rb_gc_obj_needs_cleanup_p(VALUE obj) return rb_gc_imemo_needs_cleanup_p(obj); } - shape_id_t shape_id = RBASIC_SHAPE_ID(obj); - if (rb_shape_has_fields(shape_id) && rb_shape_layout(shape_id) == SHAPE_ID_LAYOUT_OTHER) { - return true; - } - switch (flags & RUBY_T_MASK) { case T_FLOAT: case T_RATIONAL: @@ -2162,10 +2157,6 @@ rb_gc_obj_free_vm_weak_references(VALUE obj) { ASSUME(!RB_SPECIAL_CONST_P(obj)); - if (rb_obj_gen_fields_p(obj)) { - rb_free_generic_ivar(obj); - } - switch (BUILTIN_TYPE(obj)) { case T_STRING: if (FL_TEST_RAW(obj, RSTRING_FSTR)) { @@ -4048,6 +4039,20 @@ vm_weak_table_frozen_strings_foreach(VALUE *str, void *data) } void rb_fstring_foreach_with_replace(int (*callback)(VALUE *str, void *data), void *data); + +// Whether this table must be cleaned every GC after marking. +// Other tables may be skipped cleaned up per-object via rb_gc_obj_free_vm_weak_references. +bool +rb_gc_vm_weak_table_essential_p(enum rb_gc_vm_weak_tables table) +{ + switch (table) { + case RB_GC_VM_GENERIC_FIELDS_TABLE: + return true; + default: + return false; + } +} + void rb_gc_vm_weak_table_foreach(vm_table_foreach_callback_func callback, vm_table_update_callback_func update_callback, diff --git a/gc/default/default.c b/gc/default/default.c index cee550df6b6c79..e4889a007a19eb 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -4232,6 +4232,15 @@ gc_sweep_freeobj_hooks(rb_objspace_t *objspace) } } +static int +gc_sweep_weak_table_i(VALUE val, void *data) +{ + rb_objspace_t *objspace = data; + if (RB_SPECIAL_CONST_P(val)) return ST_CONTINUE; + if (RVALUE_MARKED(objspace, val)) return ST_CONTINUE; + return ST_DELETE; +} + static void gc_sweep_start(rb_objspace_t *objspace) { @@ -4242,6 +4251,17 @@ gc_sweep_start(rb_objspace_t *objspace) gc_sweep_freeobj_hooks(objspace); } + for (int table = 0; table < RB_GC_VM_WEAK_TABLE_COUNT; table++) { + if (!rb_gc_vm_weak_table_essential_p(table)) continue; + rb_gc_vm_weak_table_foreach( + gc_sweep_weak_table_i, + NULL, + objspace, + true, + table + ); + } + #if GC_CAN_COMPILE_COMPACTION if (objspace->flags.during_compacting) { gc_sort_heap_by_compare_func( diff --git a/gc/gc.h b/gc/gc.h index 3de9b3f165ddb3..7ab14af1a58fc4 100644 --- a/gc/gc.h +++ b/gc/gc.h @@ -92,6 +92,7 @@ MODULAR_GC_FN void rb_gc_vm_unlock_no_barrier(unsigned int lev, const char *file MODULAR_GC_FN void rb_gc_vm_barrier(void); MODULAR_GC_FN size_t rb_gc_obj_optimal_size(VALUE obj); MODULAR_GC_FN void rb_gc_mark_children(void *objspace, VALUE obj); +MODULAR_GC_FN bool rb_gc_vm_weak_table_essential_p(enum rb_gc_vm_weak_tables table); MODULAR_GC_FN void rb_gc_vm_weak_table_foreach(vm_table_foreach_callback_func callback, vm_table_update_callback_func update_callback, void *data, bool weak_only, enum rb_gc_vm_weak_tables table); MODULAR_GC_FN void rb_gc_update_object_references(void *objspace, VALUE obj); MODULAR_GC_FN void rb_gc_update_vm_references(void *objspace); From 83626894f7add308d9b459cf30449f25551e352e Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 1 Jul 2026 22:06:22 -0700 Subject: [PATCH 27/39] make_shareable: Avoid VM lock for decl_marking For T_DATA objects which use declarative marking or have a NULL dmark function, we can avoid taking the VM lock to iterate their children. This is probably most significant for objects like Time. --- ractor.c | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/ractor.c b/ractor.c index 40a2350f571786..ce382fad0ac285 100644 --- a/ractor.c +++ b/ractor.c @@ -1282,6 +1282,20 @@ obj_traverse_reachable_i(VALUE obj, void *ptr) } } +// Traverse obj's children via its GC mark function. Returns 1 to stop. +static int +obj_traverse_reachable(VALUE obj, struct obj_traverse_data *data) +{ + struct obj_traverse_callback_data d = { + .stop = false, + .data = data, + }; + RB_VM_LOCKING_NO_BARRIER() { + rb_objspace_reachable_objects_from(obj, obj_traverse_reachable_i, &d); + } + return d.stop; +} + static struct st_table * obj_traverse_rec(struct obj_traverse_data *data) { @@ -1393,19 +1407,31 @@ obj_traverse_i(VALUE obj, struct obj_traverse_data *data) break; case T_DATA: - case T_IMEMO: { - struct obj_traverse_callback_data d = { - .stop = false, - .data = data, - }; - RB_VM_LOCKING_NO_BARRIER() { - rb_objspace_reachable_objects_from(obj, obj_traverse_reachable_i, &d); + void *const ptr = RTYPEDDATA_GET_DATA(obj); + const rb_data_type_t *type = RTYPEDDATA_TYPE(obj); + + if (!ptr || !type->function.dmark) { + // no references (the class and ivars are handled elsewhere) + } + else if (type->flags & RUBY_TYPED_DECL_MARKING) { + const size_t *offsets = (const size_t *)(uintptr_t)type->function.dmark; + for (; *offsets != RUBY_REF_END; offsets++) { + VALUE ref = *(VALUE *)((char *)ptr + *offsets); + if (obj_traverse_i(ref, data)) return 1; + } + } + else { + if (obj_traverse_reachable(obj, data)) return 1; } - if (d.stop) return 1; } break; + case T_IMEMO: + // TODO: Not sure this can actually happen; traverse rather than crash. + if (obj_traverse_reachable(obj, data)) return 1; + break; + // unreachable case T_CLASS: case T_MODULE: From 697afb1e37f28ebd3038b75c1391d80444c5cb6a Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 2 Jul 2026 14:33:33 -0700 Subject: [PATCH 28/39] Use slightly faster versions of functions --- ractor.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ractor.c b/ractor.c index ce382fad0ac285..0ff950cf2eb340 100644 --- a/ractor.c +++ b/ractor.c @@ -1363,7 +1363,7 @@ obj_traverse_i(VALUE obj, struct obj_traverse_data *data) rb_ary_cancel_sharing(obj); for (int i = 0; i < RARRAY_LENINT(obj); i++) { - VALUE e = rb_ary_entry(obj, i); + VALUE e = RARRAY_AREF(obj, i); if (obj_traverse_i(e, data)) return 1; } } @@ -1601,7 +1601,7 @@ make_shareable_check_shareable(VALUE obj) static enum obj_traverse_iterator_result mark_shareable(VALUE obj) { - if (RB_TYPE_P(obj, T_STRING)) { + if (RB_BUILTIN_TYPE(obj) == T_STRING) { rb_str_make_independent(obj); } @@ -1939,7 +1939,7 @@ obj_traverse_replace_i(VALUE obj, struct obj_traverse_replace_data *data) rb_ary_cancel_sharing(obj); for (int i = 0; i < RARRAY_LENINT(obj); i++) { - VALUE e = rb_ary_entry(obj, i); + VALUE e = RARRAY_AREF(obj, i); if (obj_traverse_replace_i(e, data)) { return 1; From e98b8fd9ac5bede1cb31f02b8ab3c20da53cf1e9 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 2 Jul 2026 14:33:53 -0700 Subject: [PATCH 29/39] Only iterate ivars when they exist --- ractor.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ractor.c b/ractor.c index 0ff950cf2eb340..a670d49abe3166 100644 --- a/ractor.c +++ b/ractor.c @@ -1337,12 +1337,14 @@ obj_traverse_i(VALUE obj, struct obj_traverse_data *data) } RB_OBJ_WRITTEN(data->rec_hash, Qundef, obj); - struct obj_traverse_callback_data d = { - .stop = false, - .data = data, - }; - rb_ivar_foreach(obj, obj_traverse_ivar_foreach_i, (st_data_t)&d); - if (d.stop) return 1; + if (rb_obj_shape_has_ivars(obj)) { + struct obj_traverse_callback_data d = { + .stop = false, + .data = data, + }; + rb_ivar_foreach(obj, obj_traverse_ivar_foreach_i, (st_data_t)&d); + if (d.stop) return 1; + } switch (BUILTIN_TYPE(obj)) { // no child node From e51c4e62bbbb864a93fc2e55d92c7d4517d7813d Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Wed, 15 Jul 2026 21:02:46 +0100 Subject: [PATCH 30/39] ZJIT: rename gc_fast_path_new_obj -> gc_fastpath_new_obj For consistency. --- zjit/src/codegen.rs | 12 ++++++------ zjit/src/codegen/gc_fastpath.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 5c66679109372c..18f6071cb05399 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -2068,7 +2068,7 @@ fn gen_new_array( let flags = (RUBY_T_ARRAY as u64) | (RARRAY_EMBED_FLAG as u64); let klass = unsafe { rb_cArray }; - gc_fastpath::gc_fast_path_new_obj(jit, asm, alloc_size, flags, klass, |asm| { + gc_fastpath::gc_fastpath_new_obj(jit, asm, alloc_size, flags, klass, |asm| { asm_ccall!(asm, rb_ec_ary_new_from_values, EC, 0i64.into(), Opnd::UImm(0)) }) } @@ -2361,11 +2361,11 @@ fn gen_new_hash( let flags = RUBY_T_HASH as u64; let klass = unsafe { rb_cHash }; - let hash = gc_fastpath::gc_fast_path_new_obj(jit, asm, alloc_size, flags, klass, |asm| { + let hash = gc_fastpath::gc_fastpath_new_obj(jit, asm, alloc_size, flags, klass, |asm| { asm_ccall!(asm, rb_hash_new,) }); // TODO: this runs on the slow path too, where rb_hash_new already set - // ifnone. A fast-path-only init hook in gc_fast_path_new_obj would avoid + // ifnone. A fast-path-only init hook in gc_fastpath_new_obj would avoid // the redundant store and be reusable for other types. asm.store(Opnd::mem(VALUE_BITS, hash, RUBY_OFFSET_RHASH_IFNONE), Qnil.into()); hash @@ -2381,11 +2381,11 @@ fn gen_new_hash( let flags = RUBY_T_HASH as u64; let klass = unsafe { rb_cHash }; - let hash = gc_fastpath::gc_fast_path_new_obj(jit, asm, alloc_size, flags, klass, |asm| { + let hash = gc_fastpath::gc_fastpath_new_obj(jit, asm, alloc_size, flags, klass, |asm| { asm_ccall!(asm, rb_hash_new_with_size, num_pairs.into()) }); // TODO: this runs on the slow path too, where rb_hash_new already set - // ifnone. A fast-path-only init hook in gc_fast_path_new_obj would avoid + // ifnone. A fast-path-only init hook in gc_fastpath_new_obj would avoid // the redundant store and be reusable for other types. asm.store(Opnd::mem(VALUE_BITS, hash, RUBY_OFFSET_RHASH_IFNONE), Qnil.into()); hash @@ -2450,7 +2450,7 @@ fn gen_object_alloc_class(jit: &mut JITState, asm: &mut Assembler, class: VALUE, }; if has_fastpath { let flags = (RUBY_T_OBJECT as u64) | ((shape_id as u64) << RB_SHAPE_FLAG_SHIFT as u64); - gc_fastpath::gc_fast_path_new_obj(jit, asm, alloc_size, flags, class, |asm| { + gc_fastpath::gc_fastpath_new_obj(jit, asm, alloc_size, flags, class, |asm| { asm_ccall!(asm, rb_class_allocate_instance, class.into()) }) } else { diff --git a/zjit/src/codegen/gc_fastpath.rs b/zjit/src/codegen/gc_fastpath.rs index b4ce600e279d5c..1aacf183781fd5 100644 --- a/zjit/src/codegen/gc_fastpath.rs +++ b/zjit/src/codegen/gc_fastpath.rs @@ -68,7 +68,7 @@ enum PreparedNewObjFastpath { Mmtk(RbGcZjitMmtkNewObjFastpath), } -pub(super) fn gc_fast_path_new_obj( +pub(super) fn gc_fastpath_new_obj( jit: &mut JITState, asm: &mut Assembler, alloc_size: usize, From 5167ab8ad625d753d84218c7846f5412e0892604 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 16 Jul 2026 06:24:13 +0900 Subject: [PATCH 31/39] set RUBY_DEBUG_TEST_NO_REMOTE=1 for debug.gem to skip remote connection tests. --- .github/workflows/macos.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 191e3a8988bac8..ce81e15ed8a164 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -158,6 +158,7 @@ jobs: env: RUBY_TESTOPTS: '-q --tty=no' TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' + RUBY_DEBUG_TEST_NO_REMOTE: "1" PRECHECK_BUNDLED_GEMS: 'no' LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} From faa3cc8cf13df66e8956200f3c20735c9500c800 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Wed, 15 Jul 2026 21:47:55 +0000 Subject: [PATCH 32/39] Fix Ractor move source husk breaking compaction slot size invariant When an object is moved to another Ractor with `Ractor.send(obj, move: true)`, the moved-out source is neutralized into a RactorMovedObject husk by move_leave. The husk stays in the object's original, possibly larger, slot, but its shape was reset to 0, losing the capacity. Shape 0 implies the smallest slot, so the husk ended up with rb_obj_shape_slot_size(husk) != rb_gc_obj_slot_size(husk). Since re-embeddable T_OBJECT fields landed, compaction requires every live object to satisfy rb_gc_obj_slot_size == rb_obj_shape_slot_size (asserted in gc_ref_update_object under RGENGC_CHECK_MODE). A GC.compact in the receiving Ractor walks the husk and trips the assertion; in a release build it updates references with a mismatched slot width and can corrupt the slot. Give the husk an empty, frozen, RObject-layout shape while preserving its SHAPE_ID_CAPACITY_MASK, so the slot size still matches and no field is exposed. Co-Authored-By: Jean Boussier Co-Authored-By: Claude Fable 5 --- bootstraptest/test_ractor.rb | 24 ++++++++++++++++++++++++ ractor.c | 6 ++++++ 2 files changed, 30 insertions(+) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index f8c3573e1f24b5..cf3d16c2a707d3 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -2668,3 +2668,27 @@ def foo(a:, b:, c:) = super(a: a, b: b, c: c) :ok # platform without fork end } + +# A moved object's source is neutralized into a RactorMovedObject husk that +# stays in its original (possibly larger) slot. It must be given a shape whose +# slot size matches that slot, or a later compaction in the receiver trips the +# slot_size == shape_slot_size invariant (RGENGC_CHECK_MODE) / corrupts the slot. +assert_equal 'ok', %q{ + r = Ractor.new do + while (o = Ractor.receive) + begin + GC.compact + rescue NotImplementedError + # no-op on platforms without GC.compact (e.g. MMTk) + end + end + :ok + end + 500.times do + o = Object.new + 12.times { |i| o.instance_variable_set("@i#{i}", i) } # overflow to a larger slot + r.send(o, move: true) + end + r.send(nil) + r.value +} diff --git a/ractor.c b/ractor.c index a670d49abe3166..4cffe8526d6b1b 100644 --- a/ractor.c +++ b/ractor.c @@ -2109,11 +2109,17 @@ move_leave(VALUE obj, struct obj_traverse_replace_data *data) } VALUE flags = T_OBJECT | FL_FREEZE | (RBASIC(obj)->flags & FL_PROMOTED); + shape_id_t shape_id = (RBASIC_SHAPE_ID(obj) & SHAPE_ID_CAPACITY_MASK) | ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_ROBJECT | SHAPE_ID_FL_FROZEN; // Avoid mutations using bind_call, etc. MEMZERO((char *)obj, char, sizeof(struct RBasic)); RBASIC(obj)->flags = flags; RBASIC_SET_CLASS_RAW(obj, rb_cRactorMovedObject); + + // The husk keeps its original (larger) slot, so give it a field-less shape + // sized to that slot; otherwise compaction's slot_size == shape_slot_size + // invariant is violated. + RBASIC_SET_FULL_SHAPE_ID(obj, shape_id); return traverse_cont; } From c7c0483efce0cf4ddce068bbccc6a055f77b81ed Mon Sep 17 00:00:00 2001 From: Ian Ker-Seymer Date: Thu, 25 Jun 2026 15:32:00 -0400 Subject: [PATCH 33/39] ractor.c: preserve aliasing during Ractor move Moving an unshareable object graph with duplicate references failed to preserve aliasing because the traversal checked the `rec` table too late. On a move path, `move_leave` replaces the original object with a `Ractor::MovedObject` tombstone. Because `move_enter` classifies this tombstone as shareable, the second visit returned `traverse_skip` before the visited `rec` lookup was reached. Fix this by performing the `st_lookup` on `data->rec` at the top of `obj_traverse_replace_i`, before calling `enter_func`. We guard the lookup with a NULL check on `data->rec` to avoid allocating `rec_hash` for simple graphs. --- bootstraptest/test_ractor.rb | 23 +++++++++++++++++++++++ ractor.c | 18 ++++++++---------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index cf3d16c2a707d3..a288cd1149787d 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -656,6 +656,29 @@ def check obj1 end } +# move preserves aliasing inside the moved object graph +assert_equal 'true', %q{ + r = Ractor.new do + Ractor.receive + end + + leaf = +"leaf" + moved = r.send([leaf, leaf], move: true).value + moved[0].equal?(moved[1]) +} + +# move handles cyclic references safely and preserves aliasing +assert_equal 'true', %q{ + r = Ractor.new do + Ractor.receive + end + + a = [] + a << a + moved = r.send(a, move: true).value + moved.equal?(moved[0]) +} + # unshareable frozen objects should still be frozen in new ractor after move assert_equal 'true', %q{ r = Ractor.new do diff --git a/ractor.c b/ractor.c index 4cffe8526d6b1b..fbb5ae8421ecb2 100644 --- a/ractor.c +++ b/ractor.c @@ -1841,6 +1841,11 @@ obj_traverse_replace_i(VALUE obj, struct obj_traverse_replace_data *data) return 0; } + if (UNLIKELY(data->rec && st_lookup(data->rec, (st_data_t)obj, &replacement))) { + data->replacement = (VALUE)replacement; + return 0; + } + switch (data->enter_func(obj, data)) { case traverse_cont: break; case traverse_skip: return 0; // skip children @@ -1848,16 +1853,9 @@ obj_traverse_replace_i(VALUE obj, struct obj_traverse_replace_data *data) } replacement = (st_data_t)data->replacement; - - if (UNLIKELY(st_lookup(obj_traverse_replace_rec(data), (st_data_t)obj, &replacement))) { - data->replacement = (VALUE)replacement; - return 0; - } - else { - st_insert(obj_traverse_replace_rec(data), (st_data_t)obj, replacement); - RB_OBJ_WRITTEN(data->rec_hash, Qundef, obj); - RB_OBJ_WRITTEN(data->rec_hash, Qundef, replacement); - } + st_insert(obj_traverse_replace_rec(data), (st_data_t)obj, replacement); + RB_OBJ_WRITTEN(data->rec_hash, Qundef, obj); + RB_OBJ_WRITTEN(data->rec_hash, Qundef, replacement); if (!data->move) { obj = replacement; From 6e055f5692720cb783fe1bd0d5c50d8dc074ecc8 Mon Sep 17 00:00:00 2001 From: Misaki Shioi <31817032+shioimm@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:11:36 +0900 Subject: [PATCH 34/39] Fix: Avoid hangs in fast_fallback tests under scheduling jitter (#17889) Some fast_fallback tests inject an artificial delay to test timing-sensitive behavior. Under CI's parallel test execution, threadscheduling jitter can make the delay longer than expected, and in that case these tests may fail to connect to the server process they are actually supposed to connect to. For example: 1. The IPv6 name resolution is artificially delayed by 25ms, so the IPv4 name resolution succeeds first. 2. If the IPv6 name resolution succeeds within 25ms, the client starts connecting to the `::1` server process and the test passes. However, if the IPv6 name resolution is delayed by more than 50ms for some reason, fast_fallback starts connecting to `127.0.0.1` instead, by design. 3. If another test happens to have a server listening on `127.0.0.1` with the same port number, the connection accidentally succeeds. 4. The `::1` server process is left waiting, and the thread cleanup hangs. To address this, the listening server process now receives the result of `accept` through a local variable instead of `Thread#value`. On success, it joins the thread with a short timeout; otherwise, it terminates the thread with `Thread#kill.join`. This change is expected to prevent hangs even when such a wrong connection occurs. --- test/socket/test_tcp.rb | 45 ++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/test/socket/test_tcp.rb b/test/socket/test_tcp.rb index d689ab23765c81..cfa1413d3699f8 100644 --- a/test/socket/test_tcp.rb +++ b/test/socket/test_tcp.rb @@ -230,16 +230,22 @@ def test_initialize_v6_hostname_resolved_in_resolution_delay port = server.addr[1] delay_time = 25 # Socket::RESOLUTION_DELAY (private) is 50ms - server_thread = Thread.new { server.accept } + accepted = nil + server_thread = Thread.new { accepted = server.accept } socket = TCPSocket.new( "localhost", port, fast_fallback: true, test_mode_settings: { delay: { ipv6: delay_time } } ) - assert_true(socket.remote_address.ipv6?) + # Another process may be listening on the same port on 127.0.0.1. + omit "connected to an unrelated process on the same port" unless socket.remote_address.ipv6? + server_thread.join(3) + assert_not_nil(accepted, "server did not accept the connection") + assert_equal(socket.local_address.to_sockaddr, accepted.remote_address.to_sockaddr) ensure - server_thread&.value&.close + stop_accept_thread(server_thread, socket) + accepted&.close server&.close socket&.close end @@ -252,16 +258,21 @@ def test_initialize_v6_hostname_resolved_earlier_and_v6_server_is_not_listening server.bind(Socket.pack_sockaddr_in(0, ipv4_address)) port = server.connect_address.ip_port - server_thread = Thread.new { server.listen(1); server.accept } + accepted = nil + server_thread = Thread.new { server.listen(1); accepted, _ = server.accept } socket = TCPSocket.new( "localhost", port, fast_fallback: true, test_mode_settings: { delay: { ipv4: 10 } } ) - assert_equal(ipv4_address, socket.remote_address.ip_address) + # Another process may be listening on the same port on ::1. + omit "connected to an unrelated process on the same port" if socket.remote_address.ipv6? + server_thread.join(3) + assert_not_nil(accepted, "server did not accept the connection") + assert_equal(socket.local_address.to_sockaddr, accepted.remote_address.to_sockaddr) ensure - accepted, _ = server_thread&.value + stop_accept_thread(server_thread, socket) accepted&.close server&.close socket&.close @@ -274,16 +285,21 @@ def test_initialize_v6_hostname_resolved_later_and_v6_server_is_not_listening server.bind(Socket.pack_sockaddr_in(0, "127.0.0.1")) port = server.connect_address.ip_port - server_thread = Thread.new { server.listen(1); server.accept } + accepted = nil + server_thread = Thread.new { server.listen(1); accepted, _ = server.accept } socket = TCPSocket.new( "localhost", port, fast_fallback: true, test_mode_settings: { delay: { ipv6: 25 } } ) - assert_true(socket.remote_address.ipv4?) + # Another process may be listening on the same port on ::1. + omit "connected to an unrelated process on the same port" if socket.remote_address.ipv6? + server_thread.join(3) + assert_not_nil(accepted, "server did not accept the connection") + assert_equal(socket.local_address.to_sockaddr, accepted.remote_address.to_sockaddr) ensure - accepted, _ = server_thread&.value + stop_accept_thread(server_thread, socket) accepted&.close server&.close socket&.close @@ -420,4 +436,15 @@ def test_initialize_fast_fallback_is_false server&.close socket&.close end + + private + + # On success, wait for the accept thread to finish. If the test failed + # before the client connected, `server.accept` never returns, so kill + # the thread instead of waiting for it. + def stop_accept_thread(server_thread, socket) + return unless server_thread + server_thread.join(1) if socket + server_thread.kill.join + end end if defined?(TCPSocket) From 0117f717ede0566fdabc1dd29b63abf2e74db05d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 15 Jul 2026 15:27:15 +0900 Subject: [PATCH 35/39] win32: Remove dead Windows CE guard The Windows CE port was removed long ago and no supported toolchain defines `_WIN32_WCE`. --- win32/win32.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/win32/win32.c b/win32/win32.c index 00a87ff226d60c..69fc706b2e062a 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -6708,9 +6708,6 @@ rb_w32_pipe(int fds[2]) static int console_emulator_p(void) { -#ifdef _WIN32_WCE - return FALSE; -#else const void *const func = WriteConsoleW; HMODULE k; MEMORY_BASIC_INFORMATION m; @@ -6722,7 +6719,6 @@ console_emulator_p(void) k = GetModuleHandle("kernel32.dll"); if (!k) return FALSE; return (HMODULE)m.AllocationBase != k; -#endif } /* License: Ruby's */ From 87506bb30fc011f35f40ff8b6f3c5a6033fe222e Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 15 Jul 2026 15:27:46 +0900 Subject: [PATCH 36/39] win32: Remove DEC Alpha AXP detection from setup.mak Windows NT for Alpha was discontinued before Windows 2000 and no MSVC that satisfies the required `_MSC_VER >= 1900` targets Alpha. Since b5ecfd1eba the inline file for `-generic-` had lost its own terminator and swallowed the whole `-alpha-` rule up to its `<<`, feeding an `#error` line to the preprocessor whose failure was silently discarded by the `findstr` pipe. Removing the block also restores that `<<` as the terminator of the `-generic-` inline file. --- win32/setup.mak | 9 --------- 1 file changed, 9 deletions(-) diff --git a/win32/setup.mak b/win32/setup.mak index db0445302275f7..8ae944ae598854 100644 --- a/win32/setup.mak +++ b/win32/setup.mak @@ -33,7 +33,6 @@ i386-mswin32: -prologue- -i386- -epilogue- i486-mswin32: -prologue- -i486- -epilogue- i586-mswin32: -prologue- -i586- -epilogue- i686-mswin32: -prologue- -i686- -epilogue- -alpha-mswin32: -prologue- -alpha- -epilogue- x64-mswin64: -prologue- -x64- -epilogue- arm64-mswin64: -prologue- -arm64- -epilogue- @@ -233,14 +232,6 @@ MACHINE = x86 $(CPU) = $(PROCESSOR_LEVEL) !endif #endif - --alpha-: -osname32- - @$(CPP) -Tc <<"checking if compiler is for $(@:-=)" >>$(MAKEFILE) -#ifndef _M_ALPHA -#error Not compiler for $(@:-=) -#else -MACHINE = $(@:-=) -#endif << -x64-: -osname64- From 65c93458735e3738ef81356bbac65dfbd079c888 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 15 Jul 2026 15:28:12 +0900 Subject: [PATCH 37/39] win32: Reject MSVC older than 1900 at setup pass win32/Makefile.sub already errors on `_MSC_VER < 1900`, but setup.mak only rejected pre-VC8 compilers and let 1400..1899 pass silently. Fail at the setup pass with the actual minimum. --- win32/setup.mak | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/win32/setup.mak b/win32/setup.mak index 8ae944ae598854..43fe24d043f905 100644 --- a/win32/setup.mak +++ b/win32/setup.mak @@ -213,7 +213,7 @@ set /a MSC_VER_UPPER = MSC_VER/20*20+19 #elif _MSC_VER >= 1900 set /a MSC_VER_LOWER = MSC_VER/10*10+0 set /a MSC_VER_UPPER = MSC_VER/10*10+9 -#elif _MSC_VER < 1400 +#else # error Unsupported VC++ compiler #endif set MSC_VER From 7d023930670d6485fedea714f6d4b2a9737afb12 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 15 Jul 2026 15:28:35 +0900 Subject: [PATCH 38/39] win32: Remove SuperH remnant from mkexports.rb The `sh` architecture existed only for the Windows CE port. --- win32/mkexports.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/win32/mkexports.rb b/win32/mkexports.rb index 44bda94990d52f..dc628314b94d37 100755 --- a/win32/mkexports.rb +++ b/win32/mkexports.rb @@ -102,7 +102,7 @@ def each_line(objs, &block) end def each_export(objs) - noprefix = ($arch ||= nil and /^(sh|i\d86)/ !~ $arch) + noprefix = ($arch ||= nil and /^i\d86/ !~ $arch) objs = objs.collect {|s| s.tr('/', '\\')} filetype = nil objdump(objs) do |l| From 53dbc939ed85b398934de5731a19ad8dbc2cf05b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 15 Jul 2026 15:28:58 +0900 Subject: [PATCH 39/39] Remove obsolete -mno-cygwin flag GCC removed the `-mno-cygwin` option in the 4.x series, so any GCC recent enough to build Ruby rejects it and the dllwrap path for `--disable-shared` builds fails with it. --- cygwin/GNUmakefile.in | 1 - 1 file changed, 1 deletion(-) diff --git a/cygwin/GNUmakefile.in b/cygwin/GNUmakefile.in index 109baa747d9dc9..dd13b5decdc050 100644 --- a/cygwin/GNUmakefile.in +++ b/cygwin/GNUmakefile.in @@ -16,7 +16,6 @@ ifeq ($(target_os),cygwin) DLL_BASE_NAME := $(LIBRUBY_SO:.dll=) else DLL_BASE_NAME := $(RUBY_SO_NAME) - DLLWRAP += -mno-cygwin VPATH := $(VPATH):$(srcdir)/win32 ifneq ($(filter -flto%,$(LDFLAGS)),) miniruby$(EXEEXT): XLDFLAGS += -Wno-maybe-uninitialized