From 208b173c258508057a3914b816bb69679da33fbf Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 3 Mar 2026 10:57:41 +0000 Subject: [PATCH 1/4] Look up slot sizes for allocations in a table Also remove BASE_SLOT_SIZE. --- benchmark/string_concat.yml | 4 +- gc/default/default.c | 77 +++++++++++++++-------------- gc/mmtk/mmtk.c | 19 +++++-- test/-ext-/string/test_capacity.rb | 11 +++-- test/-ext-/string/test_set_len.rb | 2 +- test/.excludes-mmtk/TestObjSpace.rb | 2 +- test/objspace/test_objspace.rb | 6 +-- test/ruby/test_file_exhaustive.rb | 6 ++- test/ruby/test_gc.rb | 4 +- test/ruby/test_gc_compact.rb | 4 +- test/ruby/test_string.rb | 2 +- test/ruby/test_time.rb | 2 +- 12 files changed, 81 insertions(+), 58 deletions(-) diff --git a/benchmark/string_concat.yml b/benchmark/string_concat.yml index f11f95ee9a7891..c07fd21013f4d0 100644 --- a/benchmark/string_concat.yml +++ b/benchmark/string_concat.yml @@ -1,8 +1,8 @@ prelude: | CHUNK = "a" * 64 UCHUNK = "é" * 32 - SHORT = "a" * (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] / 2) - LONG = "a" * (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] * 2) + SHORT = "a" * (GC.stat_heap(0, :slot_size) / 2) + LONG = "a" * (GC.stat_heap(0, :slot_size) * 2) GC.disable # GC causes a lot of variance benchmark: binary_concat_7bit: | diff --git a/gc/default/default.c b/gc/default/default.c index 1c91a5e0d5b0fc..2d743475344c1c 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -687,7 +687,17 @@ size_t rb_gc_impl_obj_slot_size(VALUE obj); # endif #endif -#define BASE_SLOT_SIZE (sizeof(struct RBasic) + sizeof(VALUE[RBIMPL_RVALUE_EMBED_LEN_MAX]) + RVALUE_OVERHEAD) +#define RVALUE_SLOT_SIZE (sizeof(struct RBasic) + sizeof(VALUE[RBIMPL_RVALUE_EMBED_LEN_MAX]) + RVALUE_OVERHEAD) + +static const size_t pool_slot_sizes[HEAP_COUNT] = { + RVALUE_SLOT_SIZE, + RVALUE_SLOT_SIZE * 2, + RVALUE_SLOT_SIZE * 4, + RVALUE_SLOT_SIZE * 8, + RVALUE_SLOT_SIZE * 16, +}; + +static uint8_t size_to_heap_idx[RVALUE_SLOT_SIZE * (1 << (HEAP_COUNT - 1)) + 1]; #ifndef MAX # define MAX(a, b) (((a) > (b)) ? (a) : (b)) @@ -701,7 +711,7 @@ enum { HEAP_PAGE_ALIGN = (1UL << HEAP_PAGE_ALIGN_LOG), HEAP_PAGE_ALIGN_MASK = (~(~0UL << HEAP_PAGE_ALIGN_LOG)), HEAP_PAGE_SIZE = HEAP_PAGE_ALIGN, - HEAP_PAGE_BITMAP_LIMIT = CEILDIV(CEILDIV(HEAP_PAGE_SIZE, BASE_SLOT_SIZE), BITS_BITLENGTH), + HEAP_PAGE_BITMAP_LIMIT = CEILDIV(CEILDIV(HEAP_PAGE_SIZE, RVALUE_SLOT_SIZE), BITS_BITLENGTH), HEAP_PAGE_BITMAP_SIZE = (BITS_SIZE * HEAP_PAGE_BITMAP_LIMIT), }; #define HEAP_PAGE_ALIGN (1 << HEAP_PAGE_ALIGN_LOG) @@ -1636,7 +1646,7 @@ heap_page_add_freeobj(rb_objspace_t *objspace, struct heap_page *page, VALUE obj /* obj should belong to page */ !(page->start <= (uintptr_t)obj && (uintptr_t)obj < ((uintptr_t)page->start + (page->total_slots * page->slot_size)) && - obj % BASE_SLOT_SIZE == 0)) { + obj % pool_slot_sizes[0] == 0)) { rb_bug("heap_page_add_freeobj: %p is not rvalue.", (void *)obj); } @@ -2237,22 +2247,13 @@ heap_slot_size(unsigned char pool_id) { GC_ASSERT(pool_id < HEAP_COUNT); - size_t slot_size = (1 << pool_id) * BASE_SLOT_SIZE; - -#if RGENGC_CHECK_MODE - rb_objspace_t *objspace = rb_gc_get_objspace(); - GC_ASSERT(heaps[pool_id].slot_size == (short)slot_size); -#endif - - slot_size -= RVALUE_OVERHEAD; - - return slot_size; + return pool_slot_sizes[pool_id] - RVALUE_OVERHEAD; } bool rb_gc_impl_size_allocatable_p(size_t size) { - return size <= heap_slot_size(HEAP_COUNT - 1); + return size + RVALUE_OVERHEAD <= pool_slot_sizes[HEAP_COUNT - 1]; } static const size_t ALLOCATED_COUNT_STEP = 1024; @@ -2351,28 +2352,31 @@ ractor_cache_set_page(rb_objspace_t *objspace, rb_ractor_newobj_cache_t *cache, rb_asan_poison_object((VALUE)heap_cache->freelist); } -static inline size_t -heap_idx_for_size(size_t size) +static void +init_size_to_heap_idx(void) { - size += RVALUE_OVERHEAD; - - size_t slot_count = CEILDIV(size, BASE_SLOT_SIZE); + GC_ASSERT(pool_slot_sizes[HEAP_COUNT - 1] - RVALUE_OVERHEAD < sizeof(size_to_heap_idx)); - /* heap_idx is ceil(log2(slot_count)) */ - size_t heap_idx = 64 - nlz_int64(slot_count - 1); - - if (heap_idx >= HEAP_COUNT) { - rb_bug("heap_idx_for_size: allocation size too large " - "(size=%"PRIuSIZE"u, heap_idx=%"PRIuSIZE"u)", size, heap_idx); + for (size_t size = 0; size < sizeof(size_to_heap_idx); size++) { + size_t effective = size + RVALUE_OVERHEAD; + uint8_t idx; + for (idx = 0; idx < HEAP_COUNT; idx++) { + if (effective <= pool_slot_sizes[idx]) break; + } + size_to_heap_idx[size] = idx; } +} -#if RGENGC_CHECK_MODE - rb_objspace_t *objspace = rb_gc_get_objspace(); - GC_ASSERT(size <= (size_t)heaps[heap_idx].slot_size); - if (heap_idx > 0) GC_ASSERT(size > (size_t)heaps[heap_idx - 1].slot_size); -#endif +static inline size_t +heap_idx_for_size(size_t size) +{ + if (size < sizeof(size_to_heap_idx)) { + size_t heap_idx = size_to_heap_idx[size]; + if (RB_LIKELY(heap_idx < HEAP_COUNT)) return heap_idx; + } - return heap_idx; + rb_bug("heap_idx_for_size: allocation size too large " + "(size=%"PRIuSIZE")", size); } size_t @@ -2589,7 +2593,7 @@ is_pointer_to_heap(rb_objspace_t *objspace, const void *ptr) if (p < heap_pages_lomem || p > heap_pages_himem) return FALSE; RB_DEBUG_COUNTER_INC(gc_isptr_range); - if (p % BASE_SLOT_SIZE != 0) return FALSE; + if (p % pool_slot_sizes[0] != 0) return FALSE; RB_DEBUG_COUNTER_INC(gc_isptr_align); page = heap_page_for_ptr(objspace, (uintptr_t)ptr); @@ -3495,7 +3499,7 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit do { VALUE vp = (VALUE)p; - GC_ASSERT(vp % BASE_SLOT_SIZE == 0); + GC_ASSERT(vp % pool_slot_sizes[0] == 0); rb_asan_unpoison_object(vp, false); if (bitset & 1) { @@ -5594,7 +5598,7 @@ gc_compact_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t b do { VALUE vp = (VALUE)p; - GC_ASSERT(vp % BASE_SLOT_SIZE == 0); + GC_ASSERT(vp % pool_slot_sizes[0] == 0); if (bitset & 1) { objspace->rcompactor.considered_count_table[BUILTIN_TYPE(vp)]++; @@ -9518,12 +9522,14 @@ rb_gc_impl_objspace_init(void *objspace_ptr) for (int i = 0; i < HEAP_COUNT; i++) { rb_heap_t *heap = &heaps[i]; - heap->slot_size = (1 << i) * BASE_SLOT_SIZE; + heap->slot_size = pool_slot_sizes[i]; slot_div_magics[i] = (uint32_t)((uint64_t)UINT32_MAX / heap->slot_size + 1); ccan_list_head_init(&heap->pages); } + init_size_to_heap_idx(); + rb_darray_make_without_gc(&objspace->heap_pages.sorted, 0); rb_darray_make_without_gc(&objspace->weak_references, 0); @@ -9556,7 +9562,6 @@ rb_gc_impl_init(void) { VALUE gc_constants = rb_hash_new(); rb_hash_aset(gc_constants, ID2SYM(rb_intern("DEBUG")), GC_DEBUG ? Qtrue : Qfalse); - rb_hash_aset(gc_constants, ID2SYM(rb_intern("BASE_SLOT_SIZE")), SIZET2NUM(BASE_SLOT_SIZE - RVALUE_OVERHEAD)); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RBASIC_SIZE")), SIZET2NUM(sizeof(struct RBasic))); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVALUE_OVERHEAD")), SIZET2NUM(RVALUE_OVERHEAD)); rb_hash_aset(gc_constants, ID2SYM(rb_intern("HEAP_PAGE_BITMAP_SIZE")), SIZET2NUM(HEAP_PAGE_BITMAP_SIZE)); diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index 21d97b81efa611..9b09116cd8d0fe 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -644,7 +644,6 @@ void rb_gc_impl_init(void) { VALUE gc_constants = rb_hash_new(); - rb_hash_aset(gc_constants, ID2SYM(rb_intern("BASE_SLOT_SIZE")), SIZET2NUM(sizeof(VALUE) * 5)); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RBASIC_SIZE")), SIZET2NUM(sizeof(struct RBasic))); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVALUE_OVERHEAD")), INT2NUM(0)); rb_hash_aset(gc_constants, ID2SYM(rb_intern("RVARGC_MAX_ALLOCATE_SIZE")), LONG2FIX(MMTK_MAX_OBJ_SIZE)); @@ -1536,12 +1535,24 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) VALUE rb_gc_impl_stat_heap(void *objspace_ptr, VALUE heap_name, VALUE hash_or_sym) { + if (FIXNUM_P(heap_name) && SYMBOL_P(hash_or_sym)) { + int heap_idx = FIX2INT(heap_name); + if (heap_idx < 0 || heap_idx >= MMTK_HEAP_COUNT) { + rb_raise(rb_eArgError, "size pool index out of range"); + } + + if (hash_or_sym == ID2SYM(rb_intern("slot_size"))) { + return SIZET2NUM(heap_sizes[heap_idx]); + } + + return Qundef; + } + if (RB_TYPE_P(hash_or_sym, T_HASH)) { return hash_or_sym; } - else { - return Qundef; - } + + return Qundef; } // Miscellaneous diff --git a/test/-ext-/string/test_capacity.rb b/test/-ext-/string/test_capacity.rb index df000f7cdb8103..a23892142afa94 100644 --- a/test/-ext-/string/test_capacity.rb +++ b/test/-ext-/string/test_capacity.rb @@ -2,16 +2,17 @@ require 'test/unit' require '-test-/string' require 'rbconfig/sizeof' +require 'objspace' class Test_StringCapacity < Test::Unit::TestCase def test_capacity_embedded - assert_equal GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] - embed_header_size - 1, capa('foo') + assert_equal pool_slot_size(0) - embed_header_size - 1, capa('foo') assert_equal max_embed_len, capa('1' * max_embed_len) assert_equal max_embed_len, capa('1' * (max_embed_len - 1)) end def test_capacity_shared - sym = ("a" * GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE]).to_sym + sym = ("a" * pool_slot_size(0)).to_sym assert_equal 0, capa(sym.to_s) end @@ -47,7 +48,7 @@ def test_literal_capacity def test_capacity_frozen s = String.new("I am testing", capacity: 1000) - s << "a" * GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + s << "a" * pool_slot_size(0) s.freeze assert_equal(s.length, capa(s)) end @@ -69,6 +70,10 @@ def embed_header_size GC::INTERNAL_CONSTANTS[:RBASIC_SIZE] + RbConfig::SIZEOF['void*'] end + def pool_slot_size(_idx = 0) + Integer(ObjectSpace.dump("")[/"slot_size":(\d+)/, 1]) + end + def max_embed_len GC::INTERNAL_CONSTANTS[:RVARGC_MAX_ALLOCATE_SIZE] - embed_header_size - 1 end diff --git a/test/-ext-/string/test_set_len.rb b/test/-ext-/string/test_set_len.rb index 1531d76167c35c..41e14a293ac3c5 100644 --- a/test/-ext-/string/test_set_len.rb +++ b/test/-ext-/string/test_set_len.rb @@ -5,7 +5,7 @@ class Test_StrSetLen < Test::Unit::TestCase def setup # Make string long enough so that it is not embedded - @range_end = ("0".ord + GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE]).chr + @range_end = ("0".ord + GC.stat_heap(0, :slot_size)).chr @s0 = [*"0"..@range_end].join("").freeze @s1 = Bug::String.new(@s0) end diff --git a/test/.excludes-mmtk/TestObjSpace.rb b/test/.excludes-mmtk/TestObjSpace.rb index 703efd79bb8c69..82858b256fdcc9 100644 --- a/test/.excludes-mmtk/TestObjSpace.rb +++ b/test/.excludes-mmtk/TestObjSpace.rb @@ -1,5 +1,5 @@ exclude(:test_dump_all_full, "testing behaviour specific to default GC") exclude(:test_dump_flag_age, "testing behaviour specific to default GC") exclude(:test_dump_flags, "testing behaviour specific to default GC") -exclude(:test_dump_includes_slot_size, "can be removed when BASE_SLOT_SIZE is 32 bytes") +exclude(:test_dump_includes_slot_size, "can be removed when pool 0 slot size is 32 bytes") exclude(:test_dump_objects_dumps_page_slot_sizes, "testing behaviour specific to default GC") diff --git a/test/objspace/test_objspace.rb b/test/objspace/test_objspace.rb index d631f97d1bcad8..b1cd894a7fb4b0 100644 --- a/test/objspace/test_objspace.rb +++ b/test/objspace/test_objspace.rb @@ -33,7 +33,7 @@ def test_memsize_of_root_shared_string b = a.dup c = nil ObjectSpace.each_object(String) {|x| break c = x if a == x and x.frozen?} - rv_size = GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + rv_size = Integer(ObjectSpace.dump(a)[/"slot_size":(\d+)/, 1]) assert_equal([rv_size, rv_size, a.length + 1 + rv_size], [a, b, c].map {|x| ObjectSpace.memsize_of(x)}) end @@ -648,7 +648,7 @@ def dump_my_heap_please next if obj["type"] == "SHAPE" assert_not_nil obj["slot_size"] - assert_equal 0, obj["slot_size"] % (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) + assert_equal 0, obj["slot_size"] % GC.stat_heap(0, :slot_size) } end end @@ -707,7 +707,7 @@ def test_dump_includes_slot_size obj = klass.new dump = ObjectSpace.dump(obj) - assert_includes dump, "\"slot_size\":#{GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE]}" + assert_includes dump, "\"slot_size\":#{GC.stat_heap(0, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]}" end def test_dump_reference_addresses_match_dump_all_addresses diff --git a/test/ruby/test_file_exhaustive.rb b/test/ruby/test_file_exhaustive.rb index be9e6bd44e702d..88469b32e18fa9 100644 --- a/test/ruby/test_file_exhaustive.rb +++ b/test/ruby/test_file_exhaustive.rb @@ -897,10 +897,12 @@ def test_expand_path_memsize bug9934 = '[ruby-core:63114] [Bug #9934]' require "objspace" path = File.expand_path("/foo") - assert_operator(ObjectSpace.memsize_of(path), :<=, path.bytesize + GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE], bug9934) + slot_size = Integer(ObjectSpace.dump(path)[/"slot_size":(\d+)/, 1]) + assert_operator(ObjectSpace.memsize_of(path), :<=, path.bytesize + slot_size, bug9934) path = File.expand_path("/a"*25) + slot_size = Integer(ObjectSpace.dump(path)[/"slot_size":(\d+)/, 1]) assert_operator(ObjectSpace.memsize_of(path), :<=, - (path.bytesize + 1) * 2 + GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE], bug9934) + (path.bytesize + 1) * 2 + slot_size, bug9934) end def test_expand_path_encoding diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb index 60f04f8e10cf11..627b3227ee872a 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -230,7 +230,7 @@ def test_stat_heap GC.stat(stat) end - assert_equal (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) * (2**i), stat_heap[:slot_size] + assert_equal GC.stat_heap(0, :slot_size) * (2**i), stat_heap[:slot_size] assert_operator stat_heap[:heap_live_slots], :<=, stat[:heap_live_slots] assert_operator stat_heap[:heap_free_slots], :<=, stat[:heap_free_slots] assert_operator stat_heap[:heap_final_slots], :<=, stat[:heap_final_slots] @@ -692,7 +692,7 @@ def allocate_large_object = Array.new(10) end def test_gc_internals - assert_not_nil GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + assert_not_nil GC::INTERNAL_CONSTANTS[:HEAP_COUNT] end def test_sweep_in_finalizer diff --git a/test/ruby/test_gc_compact.rb b/test/ruby/test_gc_compact.rb index f3da8e4e138432..46b4a0605e35b4 100644 --- a/test/ruby/test_gc_compact.rb +++ b/test/ruby/test_gc_compact.rb @@ -387,7 +387,7 @@ def test_moving_strings_up_heaps GC.verify_compaction_references(expand_heap: true, toward: :empty) Fiber.new { - str = "a" * GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] * 4 + str = "a" * GC.stat_heap(0, :slot_size) * 4 $ary = STR_COUNT.times.map { +"" << str } }.resume @@ -408,7 +408,7 @@ def test_moving_strings_down_heaps GC.verify_compaction_references(expand_heap: true, toward: :empty) Fiber.new { - $ary = STR_COUNT.times.map { ("a" * GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] * 4).squeeze! } + $ary = STR_COUNT.times.map { ("a" * GC.stat_heap(0, :slot_size) * 4).squeeze! } }.resume stats = GC.verify_compaction_references(expand_heap: true, toward: :empty) diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index 2458d38ef4b80b..bc911408d6d54a 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -675,7 +675,7 @@ def test_string_interpolations_across_heaps_get_embedded omit if GC::INTERNAL_CONSTANTS[:HEAP_COUNT] == 1 require 'objspace' - base_slot_size = GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + base_slot_size = GC.stat_heap(0, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] small_obj_size = (base_slot_size / 2) large_obj_size = base_slot_size * 2 diff --git a/test/ruby/test_time.rb b/test/ruby/test_time.rb index 333edb80218a64..595e183f6c44ca 100644 --- a/test/ruby/test_time.rb +++ b/test/ruby/test_time.rb @@ -1433,7 +1433,7 @@ def test_memsize RbConfig::SIZEOF["void*"] # Same size as VALUE end sizeof_vtm = RbConfig::SIZEOF["void*"] * 4 + 8 - expect = GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + sizeof_timew + sizeof_vtm + expect = GC.stat_heap(0, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] + sizeof_timew + sizeof_vtm assert_operator ObjectSpace.memsize_of(t), :<=, expect rescue LoadError => e omit "failed to load objspace: #{e.message}" From 5001c1937e59febca6994c643a1e127fc7195f8b Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Fri, 6 Mar 2026 22:55:10 +0000 Subject: [PATCH 2/4] Compress the size_to_heap_idx table Index on 8 byte chunks instead of individual bytes. This works because all pool stot sizes are pointer aligned, so all sizes in an 8 byte range map to the same heap. --- gc/default/default.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 2d743475344c1c..a3b8a5685e0a69 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -697,7 +697,7 @@ static const size_t pool_slot_sizes[HEAP_COUNT] = { RVALUE_SLOT_SIZE * 16, }; -static uint8_t size_to_heap_idx[RVALUE_SLOT_SIZE * (1 << (HEAP_COUNT - 1)) + 1]; +static uint8_t size_to_heap_idx[RVALUE_SLOT_SIZE * (1 << (HEAP_COUNT - 1)) / 8 + 1]; #ifndef MAX # define MAX(a, b) (((a) > (b)) ? (a) : (b)) @@ -2355,23 +2355,22 @@ ractor_cache_set_page(rb_objspace_t *objspace, rb_ractor_newobj_cache_t *cache, static void init_size_to_heap_idx(void) { - GC_ASSERT(pool_slot_sizes[HEAP_COUNT - 1] - RVALUE_OVERHEAD < sizeof(size_to_heap_idx)); - - for (size_t size = 0; size < sizeof(size_to_heap_idx); size++) { - size_t effective = size + RVALUE_OVERHEAD; + for (size_t i = 0; i < sizeof(size_to_heap_idx); i++) { + size_t effective = i * 8 + RVALUE_OVERHEAD; uint8_t idx; for (idx = 0; idx < HEAP_COUNT; idx++) { if (effective <= pool_slot_sizes[idx]) break; } - size_to_heap_idx[size] = idx; + size_to_heap_idx[i] = idx; } } static inline size_t heap_idx_for_size(size_t size) { - if (size < sizeof(size_to_heap_idx)) { - size_t heap_idx = size_to_heap_idx[size]; + size_t compressed = (size + 7) >> 3; + if (compressed < sizeof(size_to_heap_idx)) { + size_t heap_idx = size_to_heap_idx[compressed]; if (RB_LIKELY(heap_idx < HEAP_COUNT)) return heap_idx; } From a2b9c8a5b45be0cad1ac8128f37af91b40fbe889 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Mon, 9 Mar 2026 11:37:59 -0400 Subject: [PATCH 3/4] ZJIT: Avoid `NoSingletonClass` patchpoint when already a singleton class The true purpose of assume_no_singleton_classes() is to retain soundness in event of the effective class of values changing. When the effective class is already a singleton class that can never happen. (Except due to IO#reopen, but that's out of scope for now.) This deletes the patchpoint for calls on `main` that happens a lot in tests. --- zjit/src/cruby.rs | 5 + zjit/src/hir.rs | 5 + zjit/src/hir/opt_tests.rs | 363 ++++++++++++++++---------------------- zjit/src/hir/tests.rs | 18 +- 4 files changed, 170 insertions(+), 221 deletions(-) diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index f762e7672d64be..38b99edb4bd3f6 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -492,6 +492,11 @@ impl VALUE { } } + pub fn is_singleton_class(self) -> bool { + // TODO(alan): clean up one of double check on T_CLASS + unsafe { RB_TYPE_P(self, RUBY_T_CLASS) && rb_zjit_singleton_class_p(self) } + } + /// Return true for a static (non-heap) Ruby symbol (RB_STATIC_SYM_P) pub fn static_sym_p(self) -> bool { let VALUE(cval) = self; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 200502af52f880..1c60d7f52a787f 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2511,6 +2511,11 @@ impl Function { // This class can never have a singleton class, so no patchpoint needed. return true; } + if klass.is_singleton_class() { + // When a value has a singleton class, its effective class can't change anymore. + // No patchpoint needed. + return true; + } if has_singleton_class_of(klass) { // We've seen a singleton class for this klass. Disable the optimization // to avoid an invalidation loop. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 2054eb2c67d910..3476626259881c 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -1226,12 +1226,11 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v20:BasicObject = SendDirect v19, 0x1038, :foo (0x1048) + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) CheckInterrupts - Return v20 + Return v19 "); } @@ -1255,13 +1254,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, baz@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v22:Fixnum[1] = Const Value(1) + v21:Fixnum[1] = Const Value(1) CheckInterrupts - Return v22 + Return v21 "); } @@ -1284,12 +1282,11 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, baz@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_cfunc_optimized_send_count CheckInterrupts - Return v20 + Return v19 "); } @@ -1323,12 +1320,11 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v11:Fixnum[1] = Const Value(1) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v21:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v22:BasicObject = SendDirect v21, 0x1038, :foo (0x1048), v11 + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v11 CheckInterrupts - Return v22 + Return v21 "); } @@ -1485,12 +1481,11 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v20:BasicObject = SendDirect v19, 0x1038, :foo (0x1048) + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) CheckInterrupts - Return v20 + Return v19 "); } @@ -1514,12 +1509,11 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v11:Fixnum[3] = Const Value(3) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, Integer@0x1008, cme:0x1010) - v21:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v22:BasicObject = SendDirect v21, 0x1038, :Integer (0x1048), v11 + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v21:BasicObject = SendDirect v20, 0x1038, :Integer (0x1048), v11 CheckInterrupts - Return v22 + Return v21 "); } @@ -1545,12 +1539,11 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[1] = Const Value(1) v13:Fixnum[2] = Const Value(2) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v24:BasicObject = SendDirect v23, 0x1038, :foo (0x1048), v11, v13 + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 CheckInterrupts - Return v24 + Return v23 "); } @@ -1576,16 +1569,14 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v24:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v25:BasicObject = SendDirect v24, 0x1038, :foo (0x1048) - PatchPoint NoSingletonClass(Object@0x1000) + v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v24:BasicObject = SendDirect v23, 0x1038, :foo (0x1048) PatchPoint MethodRedefined(Object@0x1000, bar@0x1050, cme:0x1058) - v28:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v29:BasicObject = SendDirect v28, 0x1038, :bar (0x1048) + v26:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v27:BasicObject = SendDirect v26, 0x1038, :bar (0x1048) CheckInterrupts - Return v29 + Return v27 "); } @@ -1607,12 +1598,11 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v20:BasicObject = SendDirect v19, 0x1038, :foo (0x1048) + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) CheckInterrupts - Return v20 + Return v19 "); } @@ -1635,12 +1625,11 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v11:Fixnum[3] = Const Value(3) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v21:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v22:BasicObject = SendDirect v21, 0x1038, :foo (0x1048), v11 + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v11 CheckInterrupts - Return v22 + Return v21 "); } @@ -1664,12 +1653,11 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[3] = Const Value(3) v13:Fixnum[4] = Const Value(4) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v24:BasicObject = SendDirect v23, 0x1038, :foo (0x1048), v11, v13 + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 CheckInterrupts - Return v24 + Return v23 "); } @@ -1692,24 +1680,22 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010) - v45:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v46:BasicObject = SendDirect v45, 0x1038, :target (0x1048) + v44:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v45:BasicObject = SendDirect v44, 0x1038, :target (0x1048) v14:Fixnum[10] = Const Value(10) v16:Fixnum[20] = Const Value(20) v18:Fixnum[30] = Const Value(30) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010) - v49:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v50:BasicObject = SendDirect v49, 0x1038, :target (0x1048), v14, v16, v18 + v47:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v48:BasicObject = SendDirect v47, 0x1038, :target (0x1048), v14, v16, v18 v24:Fixnum[10] = Const Value(10) v26:Fixnum[20] = Const Value(20) v28:Fixnum[30] = Const Value(30) v30:Fixnum[40] = Const Value(40) v32:Fixnum[50] = Const Value(50) v34:BasicObject = Send v6, :target, v24, v26, v28, v30, v32 # SendFallbackReason: Argument count does not match parameter count - v37:ArrayExact = NewArray v46, v50, v34 + v37:ArrayExact = NewArray v45, v48, v34 CheckInterrupts Return v37 "); @@ -1736,12 +1722,11 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:StringExact = StringCopy v11 - PatchPoint NoSingletonClass(Object@0x1008) PatchPoint MethodRedefined(Object@0x1008, puts@0x1010, cme:0x1018) - v23:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] - v24:BasicObject = CCallVariadic v23, :Kernel#puts@0x1040, v12 + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] + v23:BasicObject = CCallVariadic v22, :Kernel#puts@0x1040, v12 CheckInterrupts - Return v24 + Return v23 "); } @@ -3418,14 +3403,13 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, block_given?@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v21:CPtr = GetEP 0 - v22:BoolExact = IsBlockGiven v21 + v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v20:CPtr = GetEP 0 + v21:BoolExact = IsBlockGiven v20 IncrCounter inline_cfunc_optimized_send_count CheckInterrupts - Return v22 + Return v21 "); } @@ -3446,13 +3430,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, block_given?@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v21:FalseClass = Const Value(false) + v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v20:FalseClass = Const Value(false) IncrCounter inline_cfunc_optimized_send_count CheckInterrupts - Return v21 + Return v20 "); } @@ -3476,9 +3459,8 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, block_given?@0x1008, cme:0x1010) - v24:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_cfunc_optimized_send_count v15:Fixnum[5] = Const Value(5) CheckInterrupts @@ -3639,12 +3621,11 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[1] = Const Value(1) v13:Fixnum[2] = Const Value(2) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v24:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v25:BasicObject = SendDirect v24, 0x1038, :foo (0x1048), v11, v13 + v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v24:BasicObject = SendDirect v23, 0x1038, :foo (0x1048), v11, v13 CheckInterrupts - Return v25 + Return v24 "); } @@ -3674,9 +3655,8 @@ mod hir_opt_tests { Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): v13:Fixnum[1] = Const Value(1) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v33:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v8, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v32:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v8, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count v19:CPtr = GetEP 0 v20:BasicObject = LoadField v19, :a@0x1038 @@ -3760,12 +3740,11 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[1] = Const Value(1) v13:Fixnum[2] = Const Value(2) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v24:BasicObject = SendDirect v23, 0x1038, :foo (0x1048), v11, v13 + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 CheckInterrupts - Return v24 + Return v23 "); } @@ -3791,12 +3770,11 @@ mod hir_opt_tests { v11:Fixnum[3] = Const Value(3) v13:Fixnum[1] = Const Value(1) v15:Fixnum[2] = Const Value(2) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v26:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v27:BasicObject = SendDirect v26, 0x1038, :foo (0x1048), v13, v15, v11 + v25:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v26:BasicObject = SendDirect v25, 0x1038, :foo (0x1048), v13, v15, v11 CheckInterrupts - Return v27 + Return v26 "); } @@ -3822,12 +3800,11 @@ mod hir_opt_tests { v11:Fixnum[0] = Const Value(0) v13:Fixnum[2] = Const Value(2) v15:Fixnum[1] = Const Value(1) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v26:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v27:BasicObject = SendDirect v26, 0x1038, :foo (0x1048), v11, v15, v13 + v25:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v26:BasicObject = SendDirect v25, 0x1038, :foo (0x1048), v11, v15, v13 CheckInterrupts - Return v27 + Return v26 "); } @@ -3852,12 +3829,11 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[0] = Const Value(0) v13:Fixnum[2] = Const Value(2) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v24:BasicObject = SendDirect v23, 0x1038, :foo (0x1048), v11, v13 + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 CheckInterrupts - Return v24 + Return v23 "); } @@ -3883,19 +3859,17 @@ mod hir_opt_tests { v11:Fixnum[1] = Const Value(1) v13:Fixnum[3] = Const Value(3) v15:Fixnum[4] = Const Value(4) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v38:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v39:BasicObject = SendDirect v38, 0x1038, :foo (0x1048), v11, v13, v15 + v37:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v38:BasicObject = SendDirect v37, 0x1038, :foo (0x1048), v11, v13, v15 v20:Fixnum[1] = Const Value(1) v22:Fixnum[2] = Const Value(2) v24:Fixnum[4] = Const Value(4) v26:Fixnum[3] = Const Value(3) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v43:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v44:BasicObject = SendDirect v43, 0x1038, :foo (0x1048), v20, v22, v26, v24 - v30:ArrayExact = NewArray v39, v44 + v41:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v42:BasicObject = SendDirect v41, 0x1038, :foo (0x1048), v20, v22, v26, v24 + v30:ArrayExact = NewArray v38, v42 CheckInterrupts Return v30 "); @@ -3923,19 +3897,17 @@ mod hir_opt_tests { v11:Fixnum[1] = Const Value(1) v13:Fixnum[3] = Const Value(3) v34:Fixnum[4] = Const Value(4) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v38:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v39:BasicObject = SendDirect v38, 0x1038, :foo (0x1048), v11, v13, v34 + v37:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v38:BasicObject = SendDirect v37, 0x1038, :foo (0x1048), v11, v13, v34 v18:Fixnum[1] = Const Value(1) v20:Fixnum[2] = Const Value(2) v22:Fixnum[40] = Const Value(40) v24:Fixnum[30] = Const Value(30) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v43:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v44:BasicObject = SendDirect v43, 0x1038, :foo (0x1048), v18, v20, v24, v22 - v28:ArrayExact = NewArray v39, v44 + v41:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v42:BasicObject = SendDirect v41, 0x1038, :foo (0x1048), v18, v20, v24, v22 + v28:ArrayExact = NewArray v38, v42 CheckInterrupts Return v28 "); @@ -3961,18 +3933,16 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v11:Fixnum[6] = Const Value(6) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010) - v49:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v50:BasicObject = SendDirect v49, 0x1038, :target (0x1048), v11 + v48:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v49:BasicObject = SendDirect v48, 0x1038, :target (0x1048), v11 v16:Fixnum[10] = Const Value(10) v18:Fixnum[20] = Const Value(20) v20:Fixnum[30] = Const Value(30) v22:Fixnum[6] = Const Value(6) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010) - v53:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v54:BasicObject = SendDirect v53, 0x1038, :target (0x1048), v16, v18, v20, v22 + v51:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v52:BasicObject = SendDirect v51, 0x1038, :target (0x1048), v16, v18, v20, v22 v27:Fixnum[10] = Const Value(10) v29:Fixnum[20] = Const Value(20) v31:Fixnum[30] = Const Value(30) @@ -3980,7 +3950,7 @@ mod hir_opt_tests { v35:Fixnum[50] = Const Value(50) v37:Fixnum[60] = Const Value(60) v39:BasicObject = Send v6, :target, v27, v29, v31, v33, v35, v37 # SendFallbackReason: Too many arguments for LIR - v41:ArrayExact = NewArray v50, v54, v39 + v41:ArrayExact = NewArray v49, v52, v39 CheckInterrupts Return v41 "); @@ -4006,12 +3976,11 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v11:Fixnum[2] = Const Value(2) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v21:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v22:BasicObject = SendDirect v21, 0x1038, :foo (0x1048), v11 + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v11 CheckInterrupts - Return v22 + Return v21 "); } @@ -4116,12 +4085,11 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v17:Fixnum[1] = Const Value(1) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v21:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v22:BasicObject = SendDirect v21, 0x1038, :foo (0x1048), v17 + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v17 CheckInterrupts - Return v22 + Return v21 "); } @@ -4537,11 +4505,10 @@ mod hir_opt_tests { v12:NilClass = Const Value(nil) v15:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Array@0x1008, new@0x1009, cme:0x1010) - PatchPoint NoSingletonClass(Class@0x1038) PatchPoint MethodRedefined(Class@0x1038, new@0x1009, cme:0x1010) - v57:BasicObject = CCallVariadic v46, :Array.new@0x1040, v15 + v56:BasicObject = CCallVariadic v46, :Array.new@0x1040, v15 CheckInterrupts - Return v57 + Return v56 "); } @@ -4599,11 +4566,10 @@ mod hir_opt_tests { v43:Class[String@0x1008] = Const Value(VALUE(0x1008)) v12:NilClass = Const Value(nil) PatchPoint MethodRedefined(String@0x1008, new@0x1009, cme:0x1010) - PatchPoint NoSingletonClass(Class@0x1038) PatchPoint MethodRedefined(Class@0x1038, new@0x1009, cme:0x1010) - v54:BasicObject = CCallVariadic v43, :String.new@0x1040 + v53:BasicObject = CCallVariadic v43, :String.new@0x1040 CheckInterrupts - Return v54 + Return v53 "); } @@ -6371,16 +6337,15 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint SingleRactorMode - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, zero@0x1008, cme:0x1010) - v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v31:StaticSymbol[:b] = Const Value(VALUE(0x1038)) + v29:StaticSymbol[:b] = Const Value(VALUE(0x1038)) PatchPoint MethodRedefined(Object@0x1000, one@0x1040, cme:0x1048) - v28:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v26:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count CheckInterrupts - Return v31 + Return v29 "); } @@ -6459,7 +6424,6 @@ mod hir_opt_tests { PatchPoint StableConstantNames(0x1000, Foo) v22:Class[Foo@0x1008] = Const Value(VALUE(0x1008)) v12:Fixnum[100] = Const Value(100) - PatchPoint NoSingletonClass(Class@0x1010) PatchPoint MethodRedefined(Class@0x1010, identity@0x1018, cme:0x1020) IncrCounter inline_iseq_optimized_send_count CheckInterrupts @@ -7106,13 +7070,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v22:NilClass = Const Value(nil) + v21:NilClass = Const Value(nil) CheckInterrupts - Return v22 + Return v21 "); } @@ -7799,12 +7762,11 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] - v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048) + v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v20:BasicObject = SendDirect v19, 0x1038, :foo (0x1048) CheckInterrupts - Return v21 + Return v20 "); } @@ -8826,14 +8788,13 @@ mod hir_opt_tests { PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, Thread) v20:Class[Thread@0x1008] = Const Value(VALUE(0x1008)) - PatchPoint NoSingletonClass(Class@0x1010) PatchPoint MethodRedefined(Class@0x1010, current@0x1018, cme:0x1020) - v24:CPtr = LoadEC - v25:CPtr = LoadField v24, :thread_ptr@0x1048 - v26:BasicObject = LoadField v25, :self@0x1049 + v23:CPtr = LoadEC + v24:CPtr = LoadField v23, :thread_ptr@0x1048 + v25:BasicObject = LoadField v24, :self@0x1049 IncrCounter inline_cfunc_optimized_send_count CheckInterrupts - Return v26 + Return v25 "); } @@ -10777,12 +10738,11 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count CheckInterrupts - Return v19 + Return v18 "); } @@ -10805,13 +10765,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v22:StringExact[VALUE(0x1038)] = Const Value(VALUE(0x1038)) + v21:StringExact[VALUE(0x1038)] = Const Value(VALUE(0x1038)) CheckInterrupts - Return v22 + Return v21 "); } @@ -10833,13 +10792,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v22:NilClass = Const Value(nil) + v21:NilClass = Const Value(nil) CheckInterrupts - Return v22 + Return v21 "); } @@ -10861,13 +10819,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v22:TrueClass = Const Value(true) + v21:TrueClass = Const Value(true) CheckInterrupts - Return v22 + Return v21 "); } @@ -10889,13 +10846,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v22:FalseClass = Const Value(false) + v21:FalseClass = Const Value(false) CheckInterrupts - Return v22 + Return v21 "); } @@ -10917,13 +10873,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v22:Fixnum[0] = Const Value(0) + v21:Fixnum[0] = Const Value(0) CheckInterrupts - Return v22 + Return v21 "); } @@ -10945,13 +10900,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v22:Fixnum[1] = Const Value(1) + v21:Fixnum[1] = Const Value(1) CheckInterrupts - Return v22 + Return v21 "); } @@ -10974,9 +10928,8 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): v11:Fixnum[3] = Const Value(3) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v21:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count CheckInterrupts Return v11 @@ -11004,9 +10957,8 @@ mod hir_opt_tests { v11:Fixnum[1] = Const Value(1) v13:Fixnum[2] = Const Value(2) v15:Fixnum[3] = Const Value(3) - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v25:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v24:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count CheckInterrupts Return v15 @@ -11136,13 +11088,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v23:Fixnum[123] = Const Value(123) + v22:Fixnum[123] = Const Value(123) CheckInterrupts - Return v23 + Return v22 "); } @@ -11167,13 +11118,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v23:Fixnum[123] = Const Value(123) + v22:Fixnum[123] = Const Value(123) CheckInterrupts - Return v23 + Return v22 "); } @@ -11198,13 +11148,12 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v23:Fixnum[123] = Const Value(123) + v22:Fixnum[123] = Const Value(123) CheckInterrupts - Return v23 + Return v22 "); } @@ -11629,12 +11578,11 @@ mod hir_opt_tests { PatchPoint StableConstantNames(0x1008, String) v27:Class[String@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoEPEscape(test) - PatchPoint NoSingletonClass(Class@0x1018) PatchPoint MethodRedefined(Class@0x1018, ===@0x1020, cme:0x1028) - v31:BoolExact = IsA v10, v27 + v30:BoolExact = IsA v10, v27 IncrCounter inline_cfunc_optimized_send_count CheckInterrupts - Return v31 + Return v30 "); } @@ -11662,12 +11610,11 @@ mod hir_opt_tests { PatchPoint StableConstantNames(0x1008, Kernel) v27:ModuleExact[VALUE(0x1010)] = Const Value(VALUE(0x1010)) PatchPoint NoEPEscape(test) - PatchPoint NoSingletonClass(Module@0x1018) PatchPoint MethodRedefined(Module@0x1018, ===@0x1020, cme:0x1028) IncrCounter inline_cfunc_optimized_send_count - v32:BoolExact = CCall v27, :Module#===@0x1050, v10 + v31:BoolExact = CCall v27, :Module#===@0x1050, v10 CheckInterrupts - Return v32 + Return v31 "); } @@ -11799,7 +11746,6 @@ mod hir_opt_tests { PatchPoint StableConstantNames(0x1008, Integer) v31:Class[Integer@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoEPEscape(test) - PatchPoint NoSingletonClass(Class@0x1018) PatchPoint MethodRedefined(Class@0x1018, ===@0x1020, cme:0x1028) IncrCounter inline_cfunc_optimized_send_count v23:Fixnum[5] = Const Value(5) @@ -12189,10 +12135,9 @@ mod hir_opt_tests { IncrCounter inline_cfunc_optimized_send_count v13:StaticSymbol[:_lex_actions] = Const Value(VALUE(0x1038)) v15:TrueClass = Const Value(true) - PatchPoint NoSingletonClass(Class@0x1040) PatchPoint MethodRedefined(Class@0x1040, respond_to?@0x1048, cme:0x1050) PatchPoint MethodRedefined(Class@0x1040, _lex_actions@0x1078, cme:0x1080) - v55:TrueClass = Const Value(true) + v53:TrueClass = Const Value(true) IncrCounter inline_cfunc_optimized_send_count CheckInterrupts v26:StaticSymbol[:CORRECT] = Const Value(VALUE(0x10a8)) @@ -12227,12 +12172,11 @@ mod hir_opt_tests { IncrCounter inline_iseq_optimized_send_count v29:Class[C@0x1008] = Const Value(VALUE(0x1008)) IncrCounter inline_cfunc_optimized_send_count - PatchPoint NoSingletonClass(Class@0x1040) PatchPoint MethodRedefined(Class@0x1040, name@0x1048, cme:0x1050) IncrCounter inline_cfunc_optimized_send_count - v35:StringExact|NilClass = CCall v29, :Module#name@0x1078 + v34:StringExact|NilClass = CCall v29, :Module#name@0x1078 CheckInterrupts - Return v35 + Return v34 "); } @@ -12312,14 +12256,13 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint NoSingletonClass(Object@0x1000) PatchPoint MethodRedefined(Object@0x1000, class@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] IncrCounter inline_iseq_optimized_send_count - v23:Class[Object@0x1038] = Const Value(VALUE(0x1038)) + v22:Class[Object@0x1038] = Const Value(VALUE(0x1038)) IncrCounter inline_cfunc_optimized_send_count CheckInterrupts - Return v23 + Return v22 "); } @@ -12373,16 +12316,15 @@ mod hir_opt_tests { v65:CShape[0x1006] = Const CShape(0x1006) StoreField v60, :_shape_id@0x1003, v65 v47:Class[VMFrozenCore] = Const Value(VALUE(0x1008)) - PatchPoint NoSingletonClass(Class@0x1010) PatchPoint MethodRedefined(Class@0x1010, lambda@0x1018, cme:0x1020) - v70:BasicObject = CCallWithFrame v47, :RubyVM::FrozenCore.lambda@0x1048, block=0x1050 + v69:BasicObject = CCallWithFrame v47, :RubyVM::FrozenCore.lambda@0x1048, block=0x1050 v50:CPtr = GetEP 0 v51:BasicObject = LoadField v50, :a@0x1001 v52:BasicObject = LoadField v50, :_b@0x1002 v53:BasicObject = LoadField v50, :_c@0x1058 v54:BasicObject = LoadField v50, :formatted@0x1059 CheckInterrupts - Return v70 + Return v69 "); } @@ -13845,16 +13787,15 @@ mod hir_opt_tests { v69:Truthy = RefineType v60, Truthy IfTrue v68, bb8(v58, v59, v69, v61, v62) v71:Falsy = RefineType v60, Falsy - PatchPoint NoSingletonClass(Object@0x1020) PatchPoint MethodRedefined(Object@0x1020, lambda@0x1028, cme:0x1030) - v119:ObjectSubclass[class_exact*:Object@VALUE(0x1020)] = GuardType v58, ObjectSubclass[class_exact*:Object@VALUE(0x1020)] - v120:BasicObject = CCallWithFrame v119, :Kernel#lambda@0x1058, block=0x1060 + v118:ObjectSubclass[class_exact*:Object@VALUE(0x1020)] = GuardType v58, ObjectSubclass[class_exact*:Object@VALUE(0x1020)] + v119:BasicObject = CCallWithFrame v118, :Kernel#lambda@0x1058, block=0x1060 v75:CPtr = GetEP 0 v76:BasicObject = LoadField v75, :list@0x1001 v78:BasicObject = LoadField v75, :iter_method@0x1068 v79:BasicObject = LoadField v75, :kwsplat@0x1069 - SetLocal :sep, l0, EP@5, v120 - Jump bb8(v58, v76, v120, v78, v79) + SetLocal :sep, l0, EP@5, v119 + Jump bb8(v58, v76, v119, v78, v79) bb8(v83:BasicObject, v84:BasicObject, v85:BasicObject, v86:BasicObject, v87:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1070, CONST) diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 8e55b83c1835e3..20fe04507640de 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -121,14 +121,13 @@ mod snapshot_tests { v15:Fixnum[2] = Const Value(2) v16:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13, v15], locals: [] } v23:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v13, v15, v11], locals: [] } - PatchPoint NoSingletonClass(Object@0x1010) PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) - v26:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] - v27:BasicObject = SendDirect v26, 0x1048, :foo (0x1058), v13, v15, v11 - v18:Any = Snapshot FrameState { pc: 0x1060, stack: [v27], locals: [] } + v25:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] + v26:BasicObject = SendDirect v25, 0x1048, :foo (0x1058), v13, v15, v11 + v18:Any = Snapshot FrameState { pc: 0x1060, stack: [v26], locals: [] } PatchPoint NoTracePoint CheckInterrupts - Return v27 + Return v26 "); } @@ -158,14 +157,13 @@ mod snapshot_tests { v11:Fixnum[1] = Const Value(1) v13:Fixnum[2] = Const Value(2) v14:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13], locals: [] } - PatchPoint NoSingletonClass(Object@0x1010) PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) - v23:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] - v24:BasicObject = SendDirect v23, 0x1048, :foo (0x1058), v11, v13 - v16:Any = Snapshot FrameState { pc: 0x1060, stack: [v24], locals: [] } + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] + v23:BasicObject = SendDirect v22, 0x1048, :foo (0x1058), v11, v13 + v16:Any = Snapshot FrameState { pc: 0x1060, stack: [v23], locals: [] } PatchPoint NoTracePoint CheckInterrupts - Return v24 + Return v23 "); } From b5f3f7c504c0ba31849f8eb967a70fe9f2e3fd83 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 9 Mar 2026 12:58:19 -0400 Subject: [PATCH 4/4] [ruby/prism] Refine continuable? with algorithm in C https://github.com/ruby/prism/commit/c28810fe93 --- lib/prism/lex_compat.rb | 8 +- lib/prism/parse_result.rb | 73 ++------- prism/extension.c | 3 +- prism/parser.h | 8 + prism/prism.c | 167 ++++++++++++++++++++- prism/templates/lib/prism/serialize.rb.erb | 14 +- prism/templates/src/serialize.c.erb | 1 + test/prism/errors_test.rb | 35 ----- test/prism/result/continuable_test.rb | 124 +++++++++++++++ 9 files changed, 330 insertions(+), 103 deletions(-) create mode 100644 test/prism/result/continuable_test.rb diff --git a/lib/prism/lex_compat.rb b/lib/prism/lex_compat.rb index 0bc56ec592ad85..e1b04fc6cea1e7 100644 --- a/lib/prism/lex_compat.rb +++ b/lib/prism/lex_compat.rb @@ -43,10 +43,10 @@ class Result < Prism::Result # Create a new lex compat result object with the given values. #-- - #: (Array[lex_compat_token] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, Source source) -> void - def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) + #: (Array[lex_compat_token] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, bool continuable, Source source) -> void + def initialize(value, comments, magic_comments, data_loc, errors, warnings, continuable, source) @value = value - super(comments, magic_comments, data_loc, errors, warnings, source) + super(comments, magic_comments, data_loc, errors, warnings, continuable, source) end # Implement the hash pattern matching interface for Result. @@ -825,7 +825,7 @@ def result tokens = post_process_tokens(tokens, source, result.data_loc, bom, eof_token) - Result.new(tokens, result.comments, result.magic_comments, result.data_loc, result.errors, result.warnings, source) + Result.new(tokens, result.comments, result.magic_comments, result.data_loc, result.errors, result.warnings, result.continuable?, source) end private diff --git a/lib/prism/parse_result.rb b/lib/prism/parse_result.rb index 4898fdd435c4b6..5c4d4fcb8a14dc 100644 --- a/lib/prism/parse_result.rb +++ b/lib/prism/parse_result.rb @@ -898,13 +898,14 @@ class Result # Create a new result object with the given values. #-- - #: (Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, Source source) -> void - def initialize(comments, magic_comments, data_loc, errors, warnings, source) + #: (Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, bool continuable, Source source) -> void + def initialize(comments, magic_comments, data_loc, errors, warnings, continuable, source) @comments = comments @magic_comments = magic_comments @data_loc = data_loc @errors = errors @warnings = warnings + @continuable = continuable @source = source end @@ -961,54 +962,8 @@ def failure? #-- #: () -> bool def continuable? - return false if errors.empty? - - offset = source.source.bytesize - errors.all? { |error| CONTINUABLE.include?(error.type) || error.location.start_offset == offset } - end - - # The set of error types whose location the parser places at the opening - # token of an unclosed construct rather than at the end of the source. These - # errors always indicate incomplete input regardless of their byte position, - # so they are checked by type rather than by location. - #-- - #: Array[Symbol] - CONTINUABLE = %i[ - begin_term - begin_upcase_term - block_param_pipe_term - block_term_brace - block_term_end - case_missing_conditions - case_term - class_term - conditional_term - conditional_term_else - def_term - embdoc_term - end_upcase_term - for_term - hash_term - heredoc_term - lambda_term_brace - lambda_term_end - list_i_lower_term - list_i_upper_term - list_w_lower_term - list_w_upper_term - module_term - regexp_term - rescue_term - string_interpolated_term - string_literal_eof - symbol_term_dynamic - symbol_term_interpolated - until_term - while_term - xstring_term - ].freeze - - private_constant :CONTINUABLE + @continuable + end # Create a code units cache for the given encoding. #-- @@ -1033,10 +988,10 @@ class ParseResult < Result # Create a new parse result object with the given values. #-- - #: (ProgramNode value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, Source source) -> void - def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) + #: (ProgramNode value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, bool continuable, Source source) -> void + def initialize(value, comments, magic_comments, data_loc, errors, warnings, continuable, source) @value = value - super(comments, magic_comments, data_loc, errors, warnings, source) + super(comments, magic_comments, data_loc, errors, warnings, continuable, source) end # Implement the hash pattern matching interface for ParseResult. @@ -1077,10 +1032,10 @@ class LexResult < Result # Create a new lex result object with the given values. #-- - #: (Array[[Token, Integer]] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, Source source) -> void - def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) + #: (Array[[Token, Integer]] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, bool continuable, Source source) -> void + def initialize(value, comments, magic_comments, data_loc, errors, warnings, continuable, source) @value = value - super(comments, magic_comments, data_loc, errors, warnings, source) + super(comments, magic_comments, data_loc, errors, warnings, continuable, source) end # Implement the hash pattern matching interface for LexResult. @@ -1099,10 +1054,10 @@ class ParseLexResult < Result # Create a new parse lex result object with the given values. #-- - #: ([ProgramNode, Array[[Token, Integer]]] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, Source source) -> void - def initialize(value, comments, magic_comments, data_loc, errors, warnings, source) + #: ([ProgramNode, Array[[Token, Integer]]] value, Array[Comment] comments, Array[MagicComment] magic_comments, Location? data_loc, Array[ParseError] errors, Array[ParseWarning] warnings, bool continuable, Source source) -> void + def initialize(value, comments, magic_comments, data_loc, errors, warnings, continuable, source) @value = value - super(comments, magic_comments, data_loc, errors, warnings, source) + super(comments, magic_comments, data_loc, errors, warnings, continuable, source) end # Implement the hash pattern matching interface for ParseLexResult. diff --git a/prism/extension.c b/prism/extension.c index 7c90e488456588..147434c9759680 100644 --- a/prism/extension.c +++ b/prism/extension.c @@ -641,10 +641,11 @@ parse_result_create(VALUE class, const pm_parser_t *parser, VALUE value, rb_enco parser_data_loc(parser, source, freeze), parser_errors(parser, encoding, source, freeze), parser_warnings(parser, encoding, source, freeze), + parser->continuable ? Qtrue : Qfalse, source }; - return rb_class_new_instance_freeze(7, result_argv, class, freeze); + return rb_class_new_instance_freeze(8, result_argv, class, freeze); } /******************************************************************************/ diff --git a/prism/parser.h b/prism/parser.h index ed4871197c6f02..5ebace10c61cf3 100644 --- a/prism/parser.h +++ b/prism/parser.h @@ -895,6 +895,14 @@ struct pm_parser { /** Whether or not we're currently recovering from a syntax error. */ bool recovering; + /** + * Whether or not the source being parsed could become valid if more input + * were appended. This is set to false when the parser encounters a token + * that is definitively wrong (e.g., a stray `end` or `]`) as opposed to + * merely incomplete. + */ + bool continuable; + /** * This is very specialized behavior for when you want to parse in a context * that does not respect encoding comments. Its main use case is translating diff --git a/prism/prism.c b/prism/prism.c index 18aa841ec9bebc..6edc67b62737f4 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -22016,6 +22016,7 @@ pm_parser_init(pm_arena_t *arena, pm_parser_t *parser, const uint8_t *source, si .partial_script = false, .command_start = true, .recovering = false, + .continuable = true, .encoding_locked = false, .encoding_changed = false, .pattern_matching_newlines = false, @@ -22292,12 +22293,176 @@ pm_parser_free(pm_parser_t *parser) { } } +/** + * Returns true if the given diagnostic ID represents an error that cannot be + * fixed by appending more input. These are errors where the existing source + * contains definitively invalid syntax (as opposed to merely incomplete input). + */ +static bool +pm_parse_err_is_fatal(pm_diagnostic_id_t diag_id) { + switch (diag_id) { + case PM_ERR_ARRAY_EXPRESSION_AFTER_STAR: + case PM_ERR_BEGIN_UPCASE_BRACE: + case PM_ERR_CLASS_VARIABLE_BARE: + case PM_ERR_END_UPCASE_BRACE: + case PM_ERR_ESCAPE_INVALID_HEXADECIMAL: + case PM_ERR_ESCAPE_INVALID_UNICODE_LIST: + case PM_ERR_ESCAPE_INVALID_UNICODE_SHORT: + case PM_ERR_EXPRESSION_NOT_WRITABLE: + case PM_ERR_EXPRESSION_NOT_WRITABLE_SELF: + case PM_ERR_FLOAT_PARSE: + case PM_ERR_GLOBAL_VARIABLE_BARE: + case PM_ERR_HASH_KEY: + case PM_ERR_HEREDOC_IDENTIFIER: + case PM_ERR_INSTANCE_VARIABLE_BARE: + case PM_ERR_INVALID_BLOCK_EXIT: + case PM_ERR_INVALID_ENCODING_MAGIC_COMMENT: + case PM_ERR_INVALID_FLOAT_EXPONENT: + case PM_ERR_INVALID_NUMBER_BINARY: + case PM_ERR_INVALID_NUMBER_DECIMAL: + case PM_ERR_INVALID_NUMBER_HEXADECIMAL: + case PM_ERR_INVALID_NUMBER_OCTAL: + case PM_ERR_INVALID_NUMBER_UNDERSCORE_TRAILING: + case PM_ERR_NO_LOCAL_VARIABLE: + case PM_ERR_PARAMETER_ORDER: + case PM_ERR_STATEMENT_UNDEF: + case PM_ERR_VOID_EXPRESSION: + return true; + default: + return false; + } +} + +/** + * Determine whether the source parsed by the given parser could become valid if + * more input were appended. This is used by tools like IRB to decide whether to + * prompt for continuation or to display an error. + * + * The parser starts with continuable=true. This function scans all errors to + * detect two categories of non-continuable errors: + * + * 1. Fatal errors: errors like invalid number literals or bare global variables + * that indicate definitively invalid syntax. These are only considered fatal + * if they occur before EOF (at EOF they could be from truncated input, e.g. + * `"\x` is an incomplete hex escape). + * + * 2. Stray tokens: unexpected_token_ignore and unexpected_token_close_context + * errors indicate tokens that don't belong. A stray token is a cascade + * effect (and does not prevent continuability) if: + * + * a. A non-stray, non-fatal error appeared earlier in the error list at a + * strictly earlier source position (the stray was caused by a preceding + * parse failure, e.g. a truncated heredoc), OR + * b. The stray token is at EOF, starts after position 0 (there is valid + * code before it), and either is a single byte (likely a truncated + * token like `\`) or there are non-stray errors elsewhere. + * + * Closing delimiters (`)`, `]`, `}`) at EOF are always genuinely stray — + * they are complete tokens and cannot become part of a longer valid + * construct by appending more input. + * + * c. The stray token is `=` at the start of a line, which could be the + * beginning of `=begin` (an embedded document). The remaining bytes + * after `=` may parse as an identifier, so the error is not at EOF, + * but the construct is genuinely incomplete. + */ +static void +pm_parse_continuable(pm_parser_t *parser) { + // If there are no errors then there is nothing to continue. + if (parser->error_list.size == 0) { + parser->continuable = false; + return; + } + + if (!parser->continuable) return; + + size_t source_length = (size_t) (parser->end - parser->start); + + // First pass: check if there are any non-stray, non-fatal errors. + bool has_non_stray_error = false; + for (pm_diagnostic_t *error = (pm_diagnostic_t *) parser->error_list.head; error != NULL; error = (pm_diagnostic_t *) error->node.next) { + if (error->diag_id != PM_ERR_UNEXPECTED_TOKEN_IGNORE && error->diag_id != PM_ERR_UNEXPECTED_TOKEN_CLOSE_CONTEXT && !pm_parse_err_is_fatal(error->diag_id)) { + has_non_stray_error = true; + break; + } + } + + // Second pass: check each error. We track the minimum source position + // among non-stray, non-fatal errors seen so far in list order, which + // lets us detect cascade stray tokens. + size_t non_stray_min_start = SIZE_MAX; + + for (pm_diagnostic_t *error = (pm_diagnostic_t *) parser->error_list.head; error != NULL; error = (pm_diagnostic_t *) error->node.next) { + size_t error_start = (size_t) error->location.start; + size_t error_end = error_start + (size_t) error->location.length; + bool at_eof = error_end >= source_length; + + // Fatal errors are non-continuable unless they occur at EOF. + if (pm_parse_err_is_fatal(error->diag_id) && !at_eof) { + parser->continuable = false; + return; + } + + // Track non-stray, non-fatal error positions in list order. + if (error->diag_id != PM_ERR_UNEXPECTED_TOKEN_IGNORE && + error->diag_id != PM_ERR_UNEXPECTED_TOKEN_CLOSE_CONTEXT) { + if (error_start < non_stray_min_start) non_stray_min_start = error_start; + continue; + } + + // This is a stray token. Determine if it is a cascade effect + // of a preceding error or genuinely stray. + + // Rule (a): a non-stray error was seen earlier in the list at a + // strictly earlier position — this stray is a cascade effect. + if (non_stray_min_start < error_start) continue; + + // Rule (b): this stray is at EOF with valid code before it. + // Single-byte stray tokens at EOF (like `\` for line continuation) + // are likely truncated tokens. Multi-byte stray tokens (like the + // keyword `end`) need additional evidence that they are cascade + // effects (i.e. non-stray errors exist elsewhere). + if (at_eof && error_start > 0) { + // Exception: closing delimiters at EOF are genuinely stray. + if (error->location.length == 1) { + const uint8_t *byte = parser->start + error_start; + if (*byte == ')' || *byte == ']' || *byte == '}') { + parser->continuable = false; + return; + } + + // Single-byte non-delimiter stray at EOF: cascade. + continue; + } + + // Multi-byte stray at EOF: cascade only if there are + // non-stray errors (evidence of a preceding parse failure). + if (has_non_stray_error) continue; + } + + // Rule (c): a stray `=` at the start of a line could be the + // beginning of an embedded document (`=begin`). The remaining + // bytes after `=` parse as an identifier, so the error is not + // at EOF, but the construct is genuinely incomplete. + if (error->location.length == 1) { + const uint8_t *byte = parser->start + error_start; + if (*byte == '=' && (error_start == 0 || *(byte - 1) == '\n')) continue; + } + + // This stray token is genuinely non-continuable. + parser->continuable = false; + return; + } +} + /** * Parse the Ruby source associated with the given parser and return the tree. */ PRISM_EXPORTED_FUNCTION pm_node_t * pm_parse(pm_parser_t *parser) { - return parse_program(parser); + pm_node_t *node = parse_program(parser); + pm_parse_continuable(parser); + return node; } /** diff --git a/prism/templates/lib/prism/serialize.rb.erb b/prism/templates/lib/prism/serialize.rb.erb index 4e61c89a8906e9..c272d84bb4f889 100644 --- a/prism/templates/lib/prism/serialize.rb.erb +++ b/prism/templates/lib/prism/serialize.rb.erb @@ -43,6 +43,7 @@ module Prism data_loc = loader.load_optional_location_object(freeze) errors = loader.load_errors(encoding, freeze) warnings = loader.load_warnings(encoding, freeze) + continuable = loader.load_bool cpool_base = loader.load_uint32 cpool_size = loader.load_varuint @@ -52,7 +53,7 @@ module Prism loader.load_constant_pool(constant_pool) raise unless loader.eof? - result = ParseResult.new(node, comments, magic_comments, data_loc, errors, warnings, source) + result = ParseResult.new(node, comments, magic_comments, data_loc, errors, warnings, continuable, source) result.freeze if freeze input.force_encoding(encoding) @@ -97,9 +98,10 @@ module Prism data_loc = loader.load_optional_location_object(freeze) errors = loader.load_errors(encoding, freeze) warnings = loader.load_warnings(encoding, freeze) + continuable = loader.load_bool raise unless loader.eof? - result = LexResult.new(tokens, comments, magic_comments, data_loc, errors, warnings, source) + result = LexResult.new(tokens, comments, magic_comments, data_loc, errors, warnings, continuable, source) tokens.each do |token| token[0].value.force_encoding(encoding) @@ -168,6 +170,7 @@ module Prism data_loc = loader.load_optional_location_object(freeze) errors = loader.load_errors(encoding, freeze) warnings = loader.load_warnings(encoding, freeze) + continuable = loader.load_bool cpool_base = loader.load_uint32 cpool_size = loader.load_varuint @@ -178,7 +181,7 @@ module Prism raise unless loader.eof? value = [node, tokens] #: [ProgramNode, Array[[Token, Integer]]] - result = ParseLexResult.new(value, comments, magic_comments, data_loc, errors, warnings, source) + result = ParseLexResult.new(value, comments, magic_comments, data_loc, errors, warnings, continuable, source) tokens.each do |token| token[0].value.force_encoding(encoding) @@ -488,6 +491,11 @@ module Prism (io.read(8) or raise).unpack1("D") #: Float end + #: () -> bool + def load_bool + (io.getbyte or raise) != 0 + end + #: () -> Integer def load_uint32 (io.read(4) or raise).unpack1("L") #: Integer diff --git a/prism/templates/src/serialize.c.erb b/prism/templates/src/serialize.c.erb index 4fe0cb88c1e255..1f90a2160ea4d2 100644 --- a/prism/templates/src/serialize.c.erb +++ b/prism/templates/src/serialize.c.erb @@ -247,6 +247,7 @@ pm_serialize_metadata(pm_parser_t *parser, pm_buffer_t *buffer) { pm_serialize_data_loc(parser, buffer); pm_serialize_diagnostic_list(&parser->error_list, buffer); pm_serialize_diagnostic_list(&parser->warning_list, buffer); + pm_buffer_append_byte(buffer, (uint8_t) parser->continuable); } #line <%= __LINE__ + 1 %> "prism/templates/src/<%= File.basename(__FILE__) %>" diff --git a/test/prism/errors_test.rb b/test/prism/errors_test.rb index c3362eaaf56e5c..9a54203f74a202 100644 --- a/test/prism/errors_test.rb +++ b/test/prism/errors_test.rb @@ -105,41 +105,6 @@ def test_unclosed_heredoc_and_interpolation assert_nil(statement.parts[0].statements) end - def test_continuable - # Valid input is not continuable (nothing to continue). - refute_predicate Prism.parse("1 + 1"), :continuable? - refute_predicate Prism.parse(""), :continuable? - - # Stray closing tokens make input non-continuable regardless of what - # follows (matches the feature-request examples exactly). - refute_predicate Prism.parse("1 + ]"), :continuable? - refute_predicate Prism.parse("end.tap do"), :continuable? - - # Unclosed constructs are continuable. - assert_predicate Prism.parse("1 + ["), :continuable? - assert_predicate Prism.parse("tap do"), :continuable? - - # Unclosed keywords. - assert_predicate Prism.parse("def foo"), :continuable? - assert_predicate Prism.parse("class Foo"), :continuable? - assert_predicate Prism.parse("module Foo"), :continuable? - assert_predicate Prism.parse("if true"), :continuable? - assert_predicate Prism.parse("while true"), :continuable? - assert_predicate Prism.parse("begin"), :continuable? - assert_predicate Prism.parse("for x in [1]"), :continuable? - - # Unclosed delimiters. - assert_predicate Prism.parse("{"), :continuable? - assert_predicate Prism.parse("foo("), :continuable? - assert_predicate Prism.parse('"hello'), :continuable? - assert_predicate Prism.parse("'hello"), :continuable? - assert_predicate Prism.parse("<<~HEREDOC\nhello"), :continuable? - - # A mix: stray end plus an unclosed block is not continuable because the - # stray end cannot be fixed by appending more input. - refute_predicate Prism.parse("end\ntap do"), :continuable? - end - private def assert_errors(filepath, version) diff --git a/test/prism/result/continuable_test.rb b/test/prism/result/continuable_test.rb new file mode 100644 index 00000000000000..353355216788a5 --- /dev/null +++ b/test/prism/result/continuable_test.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module Prism + class ContinuableTest < TestCase + def test_valid_input + # Valid input is not continuable (nothing to continue). + refute_predicate Prism.parse("1 + 1"), :continuable? + refute_predicate Prism.parse(""), :continuable? + end + + def test_stray_closing_tokens + # Stray closing tokens make input non-continuable regardless of what + # follows (matches the feature-request examples exactly). + refute_predicate Prism.parse("1 + ]"), :continuable? + refute_predicate Prism.parse("end.tap do"), :continuable? + + # A mix: stray end plus an unclosed block is not continuable because the + # stray end cannot be fixed by appending more input. + refute_predicate Prism.parse("end\ntap do"), :continuable? + end + + def test_unclosed_constructs + # Unclosed constructs are continuable. + assert_predicate Prism.parse("1 + ["), :continuable? + assert_predicate Prism.parse("tap do"), :continuable? + end + + def test_unclosed_keywords + assert_predicate Prism.parse("def foo"), :continuable? + assert_predicate Prism.parse("class Foo"), :continuable? + assert_predicate Prism.parse("module Foo"), :continuable? + assert_predicate Prism.parse("if true"), :continuable? + assert_predicate Prism.parse("while true"), :continuable? + assert_predicate Prism.parse("begin"), :continuable? + assert_predicate Prism.parse("for x in [1]"), :continuable? + end + + def test_unclosed_delimiters + assert_predicate Prism.parse("{"), :continuable? + assert_predicate Prism.parse("foo("), :continuable? + assert_predicate Prism.parse('"hello'), :continuable? + assert_predicate Prism.parse("'hello"), :continuable? + assert_predicate Prism.parse("<<~HEREDOC\nhello"), :continuable? + end + + def test_trailing_whitespace + # Trailing whitespace or newlines should not affect continuability. + assert_predicate Prism.parse("class A\n"), :continuable? + assert_predicate Prism.parse("def f "), :continuable? + assert_predicate Prism.parse("def f\n"), :continuable? + assert_predicate Prism.parse("def f\n "), :continuable? + assert_predicate Prism.parse("( "), :continuable? + assert_predicate Prism.parse("(\n"), :continuable? + assert_predicate Prism.parse("1 +\n"), :continuable? + end + + def test_incomplete_expressions + assert_predicate Prism.parse("-"), :continuable? + assert_predicate Prism.parse("[1,"), :continuable? + assert_predicate Prism.parse("f arg1,"), :continuable? + assert_predicate Prism.parse("def f ="), :continuable? + assert_predicate Prism.parse("def $a"), :continuable? + assert_predicate Prism.parse("a ="), :continuable? + assert_predicate Prism.parse("a,b"), :continuable? + end + + def test_modifier_keywords + assert_predicate Prism.parse("return if"), :continuable? + assert_predicate Prism.parse("return unless"), :continuable? + assert_predicate Prism.parse("while"), :continuable? + assert_predicate Prism.parse("until"), :continuable? + end + + def test_ternary_operator + assert_predicate Prism.parse("x ?"), :continuable? + assert_predicate Prism.parse("x ? y :"), :continuable? + end + + def test_class_with_superclass + assert_predicate Prism.parse("class Foo <"), :continuable? + end + + def test_keyword_expressions + assert_predicate Prism.parse("not"), :continuable? + assert_predicate Prism.parse("defined?"), :continuable? + assert_predicate Prism.parse("module"), :continuable? + end + + def test_for_loops + assert_predicate Prism.parse("for"), :continuable? + assert_predicate Prism.parse("for x in"), :continuable? + end + + def test_pattern_matching + assert_predicate Prism.parse("foo => ["), :continuable? + assert_predicate Prism.parse("case foo; when"), :continuable? + end + + def test_splat_and_block_pass + assert_predicate Prism.parse("[*"), :continuable? + assert_predicate Prism.parse("f(**"), :continuable? + assert_predicate Prism.parse("f(&"), :continuable? + end + + def test_default_parameter_value + assert_predicate Prism.parse("def f(x ="), :continuable? + end + + def test_line_continuation + assert_predicate Prism.parse("1 +\\"), :continuable? + assert_predicate Prism.parse("\"foo\" \\"), :continuable? + end + + def test_embedded_document + # Embedded document (=begin) truncated at various points. + assert_predicate Prism.parse("=b"), :continuable? + assert_predicate Prism.parse("=beg"), :continuable? + assert_predicate Prism.parse("=begin"), :continuable? + assert_predicate Prism.parse("foo\n=b"), :continuable? + end + end +end