From 2edfc51acf55bcb1db907b8e5542564529facb19 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 27 Aug 2025 09:54:40 -0400 Subject: [PATCH 01/22] Make Thread::Queue and SizedQueue support compaction --- thread_sync.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/thread_sync.c b/thread_sync.c index 701eb581d1cbc7..dd54e8267195cd 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -694,12 +694,12 @@ struct rb_szqueue { } RBIMPL_ATTR_PACKED_STRUCT_UNALIGNED_END(); static void -queue_mark(void *ptr) +queue_mark_and_move(void *ptr) { struct rb_queue *q = ptr; /* no need to mark threads in waitq, they are on stack */ - rb_gc_mark(q->que); + rb_gc_mark_and_move((VALUE *)UNALIGNED_MEMBER_PTR(q, que)); } static size_t @@ -710,7 +710,7 @@ queue_memsize(const void *ptr) static const rb_data_type_t queue_data_type = { "queue", - {queue_mark, RUBY_TYPED_DEFAULT_FREE, queue_memsize,}, + {queue_mark_and_move, RUBY_TYPED_DEFAULT_FREE, queue_memsize, queue_mark_and_move}, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY|RUBY_TYPED_WB_PROTECTED }; @@ -770,11 +770,11 @@ queue_timeout2hrtime(VALUE timeout) } static void -szqueue_mark(void *ptr) +szqueue_mark_and_move(void *ptr) { struct rb_szqueue *sq = ptr; - queue_mark(&sq->q); + queue_mark_and_move(&sq->q); } static size_t @@ -785,7 +785,7 @@ szqueue_memsize(const void *ptr) static const rb_data_type_t szqueue_data_type = { "sized_queue", - {szqueue_mark, RUBY_TYPED_DEFAULT_FREE, szqueue_memsize,}, + {szqueue_mark_and_move, RUBY_TYPED_DEFAULT_FREE, szqueue_memsize, szqueue_mark_and_move}, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY|RUBY_TYPED_WB_PROTECTED }; From 6b7ed4cad208ab7528070a66b1c97906606fa499 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 27 Aug 2025 19:18:44 +0100 Subject: [PATCH 02/22] ZJIT: Add zjit-test-update for updating insta snapshot --- zjit/.gitignore | 1 + zjit/zjit.mk | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/zjit/.gitignore b/zjit/.gitignore index 2f7896d1d1365e..100c0dfb5c031d 100644 --- a/zjit/.gitignore +++ b/zjit/.gitignore @@ -1 +1,2 @@ target/ +*.pending-snap diff --git a/zjit/zjit.mk b/zjit/zjit.mk index 43caa2e35a0ba0..e395207a9bdd2b 100644 --- a/zjit/zjit.mk +++ b/zjit/zjit.mk @@ -59,7 +59,7 @@ ZJIT_BINDGEN_DIFF_OPTS = # Generate Rust bindings. See source for details. # Needs `./configure --enable-zjit=dev` and Clang. ifneq ($(strip $(CARGO)),) # if configure found Cargo -.PHONY: zjit-bindgen zjit-bindgen-show-unused zjit-test zjit-test-lldb +.PHONY: zjit-bindgen zjit-bindgen-show-unused zjit-test zjit-test-lldb zjit-test-update zjit-bindgen: zjit.$(OBJEXT) ZJIT_SRC_ROOT_PATH='$(top_srcdir)' BINDGEN_JIT_NAME=zjit $(CARGO) run --manifest-path '$(top_srcdir)/zjit/bindgen/Cargo.toml' -- $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(Q) if [ 'x$(HAVE_GIT)' = xyes ]; then $(GIT) -C "$(top_srcdir)" diff $(ZJIT_BINDGEN_DIFF_OPTS) zjit/src/cruby_bindings.inc.rs; fi @@ -77,10 +77,23 @@ ZJIT_NEXTEST_ENV := RUBY_BUILD_DIR='$(TOP_BUILD_DIR)' \ # On darwin, it's available through `brew install cargo-nextest`. See # https://nexte.st/docs/installation/pre-built-binaries/ otherwise. zjit-test: libminiruby.a + @set +e; \ $(ZJIT_NEXTEST_ENV) $(CARGO) nextest run \ --manifest-path '$(top_srcdir)/zjit/Cargo.toml' \ '--features=$(ZJIT_TEST_FEATURES)' \ - $(ZJIT_TESTS) + $(ZJIT_TESTS); \ + exit_code=$$?; \ + if [ -f '$(top_srcdir)/zjit/src/.hir.rs.pending-snap' ]; then \ + echo ""; \ + echo "Pending snapshots found. Accept with: make zjit-test-update"; \ + fi; \ + exit $$exit_code + +# Accept all pending snapshots (requires cargo-insta) +# Install with: cargo install cargo-insta +zjit-test-update: + @$(CARGO) insta --version >/dev/null 2>&1 || { echo "Error: cargo-insta is not installed. Install with: cargo install cargo-insta"; exit 1; } + @$(CARGO) insta accept --manifest-path '$(top_srcdir)/zjit/Cargo.toml' # Run a ZJIT test written with Rust #[test] under LLDB zjit-test-lldb: libminiruby.a From 89e18473a9ef3d06a864855e19536cc2f3188f17 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 27 Aug 2025 19:24:05 +0100 Subject: [PATCH 03/22] ZJIT: Update doc about snapshot update --- doc/zjit.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/doc/zjit.md b/doc/zjit.md index 90f890bfa0a25a..ef15c6dac471ac 100644 --- a/doc/zjit.md +++ b/doc/zjit.md @@ -26,7 +26,9 @@ in a way that can be easily shared with other team members. ## Testing -Make sure you have a `--enable-zjit=dev` build, and run `brew install cargo-nextest` first. +Make sure you have a `--enable-zjit=dev` build, and install the following tools: +- `brew install cargo-nextest` - Required for running tests +- `cargo install cargo-insta` - Required for updating snapshots ### make zjit-check @@ -38,7 +40,7 @@ make zjit-check ### make zjit-test -This command runs Rust unit tests. +This command runs Rust unit tests using `insta` for snapshot testing. ``` make zjit-test @@ -50,12 +52,24 @@ You can also run a single test case by specifying the function name: make zjit-test ZJIT_TESTS=test_putobject ``` -If you expect that your changes cause tests to fail and they do, you can have -`expect-test` fix the expected value for you by putting `UPDATE_EXPECT=1` -before your test command, like so: +#### Snapshot Testing + +ZJIT uses [insta](https://insta.rs/) for snapshot testing. When tests fail due to snapshot mismatches, pending snapshots are created. The test command will notify you if there are pending snapshots: + +``` +Pending snapshots found. Accept with: make zjit-test-update +``` + +To update/accept all the snapshot changes: + +``` +make zjit-test-update +``` + +You can also review snapshot changes interactively one by one: ``` -UPDATE_EXPECT=1 make zjit-test ZJIT_TESTS=test_putobject +cd zjit && cargo insta review ``` Test changes will be reviewed alongside code changes. From d19a1eee815b5c4d14ec0ff992e7880e8a1861c5 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 27 Aug 2025 23:52:52 +0100 Subject: [PATCH 04/22] ZJIT: Enable no-fail-fast on zjit-test --- zjit/zjit.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/zjit/zjit.mk b/zjit/zjit.mk index e395207a9bdd2b..c39abf28263efe 100644 --- a/zjit/zjit.mk +++ b/zjit/zjit.mk @@ -80,6 +80,7 @@ zjit-test: libminiruby.a @set +e; \ $(ZJIT_NEXTEST_ENV) $(CARGO) nextest run \ --manifest-path '$(top_srcdir)/zjit/Cargo.toml' \ + --no-fail-fast \ '--features=$(ZJIT_TEST_FEATURES)' \ $(ZJIT_TESTS); \ exit_code=$$?; \ From 05fc01188a5315a5de034122f6c4864702f80378 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 28 Aug 2025 14:08:56 +0200 Subject: [PATCH 05/22] rb_ivar_delete: allow complex transition `rb_ivar_delete` would force a new shape to be created. And in the case of a shape exhaustion, it wouldn't handle T_CLASS/T_MODULE correctly. --- shape.c | 137 ++++++++++++++--------------- test/ruby/test_shapes.rb | 27 ++++++ variable.c | 181 ++++++++++++++++++--------------------- 3 files changed, 177 insertions(+), 168 deletions(-) diff --git a/shape.c b/shape.c index 599b77422658c7..d6b5c3040881d2 100644 --- a/shape.c +++ b/shape.c @@ -656,42 +656,6 @@ get_next_shape_internal(rb_shape_t *shape, ID id, enum shape_type shape_type, bo return res; } -static rb_shape_t * -remove_shape_recursive(rb_shape_t *shape, ID id, rb_shape_t **removed_shape) -{ - if (shape->parent_id == INVALID_SHAPE_ID) { - // We've hit the top of the shape tree and couldn't find the - // IV we wanted to remove, so return NULL - *removed_shape = NULL; - return NULL; - } - else { - if (shape->type == SHAPE_IVAR && shape->edge_name == id) { - *removed_shape = shape; - - return RSHAPE(shape->parent_id); - } - else { - // This isn't the IV we want to remove, keep walking up. - rb_shape_t *new_parent = remove_shape_recursive(RSHAPE(shape->parent_id), id, removed_shape); - - // We found a new parent. Create a child of the new parent that - // has the same attributes as this shape. - if (new_parent) { - bool dont_care; - rb_shape_t *new_child = get_next_shape_internal(new_parent, shape->edge_name, shape->type, &dont_care, true); - RUBY_ASSERT(!new_child || new_child->capacity <= shape->capacity); - return new_child; - } - else { - // We went all the way to the top of the shape tree and couldn't - // find an IV to remove so return NULL. - return NULL; - } - } - } -} - static inline shape_id_t transition_complex(shape_id_t shape_id); static shape_id_t @@ -758,34 +722,6 @@ transition_complex(shape_id_t shape_id) return next_shape_id; } -shape_id_t -rb_shape_transition_remove_ivar(VALUE obj, ID id, shape_id_t *removed_shape_id) -{ - shape_id_t original_shape_id = RBASIC_SHAPE_ID(obj); - - RUBY_ASSERT(!rb_shape_too_complex_p(original_shape_id)); - RUBY_ASSERT(!shape_frozen_p(original_shape_id)); - - rb_shape_t *removed_shape = NULL; - rb_shape_t *new_shape = remove_shape_recursive(RSHAPE(original_shape_id), id, &removed_shape); - - if (removed_shape) { - *removed_shape_id = raw_shape_id(removed_shape); - } - - if (new_shape) { - return shape_id(new_shape, original_shape_id); - } - else if (removed_shape) { - // We found the shape to remove, but couldn't create a new variation. - // We must transition to TOO_COMPLEX. - shape_id_t next_shape_id = transition_complex(original_shape_id); - RUBY_ASSERT(rb_shape_has_object_id(next_shape_id) == rb_shape_has_object_id(original_shape_id)); - return next_shape_id; - } - return original_shape_id; -} - shape_id_t rb_shape_transition_frozen(VALUE obj) { @@ -865,7 +801,7 @@ shape_get_iv_index(rb_shape_t *shape, ID id, attr_index_t *value) } static inline rb_shape_t * -shape_get_next(rb_shape_t *shape, VALUE obj, ID id, bool emit_warnings) +shape_get_next(rb_shape_t *shape, enum shape_type shape_type, VALUE obj, ID id, bool emit_warnings) { RUBY_ASSERT(!is_instance_id(id) || RTEST(rb_sym2str(ID2SYM(id)))); @@ -895,7 +831,7 @@ shape_get_next(rb_shape_t *shape, VALUE obj, ID id, bool emit_warnings) bool allow_new_shape = RCLASS_VARIATION_COUNT(klass) < SHAPE_MAX_VARIATIONS; bool variation_created = false; - rb_shape_t *new_shape = get_next_shape_internal(shape, id, SHAPE_IVAR, &variation_created, allow_new_shape); + rb_shape_t *new_shape = get_next_shape_internal(shape, id, shape_type, &variation_created, allow_new_shape); if (!new_shape) { // We could create a new variation, transitioning to TOO_COMPLEX. @@ -926,13 +862,78 @@ shape_get_next(rb_shape_t *shape, VALUE obj, ID id, bool emit_warnings) return new_shape; } +static rb_shape_t * +remove_shape_recursive(VALUE obj, rb_shape_t *shape, ID id, rb_shape_t **removed_shape) +{ + if (shape->parent_id == INVALID_SHAPE_ID) { + // We've hit the top of the shape tree and couldn't find the + // IV we wanted to remove, so return NULL + *removed_shape = NULL; + return NULL; + } + else { + if (shape->type == SHAPE_IVAR && shape->edge_name == id) { + *removed_shape = shape; + + return RSHAPE(shape->parent_id); + } + else { + // This isn't the IV we want to remove, keep walking up. + rb_shape_t *new_parent = remove_shape_recursive(obj, RSHAPE(shape->parent_id), id, removed_shape); + + // We found a new parent. Create a child of the new parent that + // has the same attributes as this shape. + if (new_parent) { + rb_shape_t *new_child = shape_get_next(new_parent, shape->type, obj, shape->edge_name, true); + RUBY_ASSERT(!new_child || new_child->capacity <= shape->capacity); + return new_child; + } + else { + // We went all the way to the top of the shape tree and couldn't + // find an IV to remove so return NULL. + return NULL; + } + } + } +} + +shape_id_t +rb_shape_transition_remove_ivar(VALUE obj, ID id, shape_id_t *removed_shape_id) +{ + shape_id_t original_shape_id = RBASIC_SHAPE_ID(obj); + RUBY_ASSERT(!shape_frozen_p(original_shape_id)); + + if (rb_shape_too_complex_p(original_shape_id)) { + return original_shape_id; + } + + rb_shape_t *removed_shape = NULL; + rb_shape_t *new_shape = remove_shape_recursive(obj, RSHAPE(original_shape_id), id, &removed_shape); + + if (removed_shape) { + *removed_shape_id = raw_shape_id(removed_shape); + } + + if (new_shape) { + return shape_id(new_shape, original_shape_id); + } + else if (removed_shape) { + // We found the shape to remove, but couldn't create a new variation. + // We must transition to TOO_COMPLEX. + shape_id_t next_shape_id = transition_complex(original_shape_id); + RUBY_ASSERT(rb_shape_has_object_id(next_shape_id) == rb_shape_has_object_id(original_shape_id)); + return next_shape_id; + } + return original_shape_id; +} + shape_id_t rb_shape_transition_add_ivar(VALUE obj, ID id) { shape_id_t original_shape_id = RBASIC_SHAPE_ID(obj); RUBY_ASSERT(!shape_frozen_p(original_shape_id)); - rb_shape_t *next_shape = shape_get_next(RSHAPE(original_shape_id), obj, id, true); + rb_shape_t *next_shape = shape_get_next(RSHAPE(original_shape_id), SHAPE_IVAR, obj, id, true); if (next_shape) { return shape_id(next_shape, original_shape_id); } @@ -947,7 +948,7 @@ rb_shape_transition_add_ivar_no_warnings(VALUE obj, ID id) shape_id_t original_shape_id = RBASIC_SHAPE_ID(obj); RUBY_ASSERT(!shape_frozen_p(original_shape_id)); - rb_shape_t *next_shape = shape_get_next(RSHAPE(original_shape_id), obj, id, false); + rb_shape_t *next_shape = shape_get_next(RSHAPE(original_shape_id), SHAPE_IVAR, obj, id, false); if (next_shape) { return shape_id(next_shape, original_shape_id); } diff --git a/test/ruby/test_shapes.rb b/test/ruby/test_shapes.rb index 08a841d25492b1..453ca8f6a72dd0 100644 --- a/test/ruby/test_shapes.rb +++ b/test/ruby/test_shapes.rb @@ -2,6 +2,7 @@ require 'test/unit' require 'objspace' require 'json' +require 'securerandom' # These test the functionality of object shapes class TestShapes < Test::Unit::TestCase @@ -1182,4 +1183,30 @@ def ensure_complex tc.send("a#{_1}_m") end end + + def assert_too_complex_during_delete(obj) + obj.instance_variable_set("@___#{SecureRandom.hex}", 1) + + (RubyVM::Shape::SHAPE_MAX_VARIATIONS * 2).times do |i| + obj.instance_variable_set("@ivar#{i}", i) + end + + refute_predicate RubyVM::Shape.of(obj), :too_complex? + (RubyVM::Shape::SHAPE_MAX_VARIATIONS * 2).times do |i| + obj.remove_instance_variable("@ivar#{i}") + end + assert_predicate RubyVM::Shape.of(obj), :too_complex? + end + + def test_object_too_complex_during_delete + assert_too_complex_during_delete(Class.new.new) + end + + def test_class_too_complex_during_delete + assert_too_complex_during_delete(Module.new) + end + + def test_generic_too_complex_during_delete + assert_too_complex_during_delete(Class.new(Array).new) + end end if defined?(RubyVM::Shape) diff --git a/variable.c b/variable.c index 8964b8e5edd9b2..690b7a26cf4997 100644 --- a/variable.c +++ b/variable.c @@ -1305,6 +1305,11 @@ rb_obj_set_fields(VALUE obj, VALUE fields_obj, ID field_name, VALUE original_fie { ivar_ractor_check(obj, field_name); + if (!fields_obj) { + rb_free_generic_ivar(obj); + return; + } + RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); RUBY_ASSERT(!original_fields_obj || IMEMO_TYPE_P(original_fields_obj, imemo_fields)); @@ -1465,14 +1470,11 @@ rb_attr_get(VALUE obj, ID id) } void rb_obj_copy_fields_to_hash_table(VALUE obj, st_table *table); +static VALUE imemo_fields_complex_from_obj(VALUE owner, VALUE source_fields_obj, shape_id_t shape_id); static shape_id_t obj_transition_too_complex(VALUE obj, st_table *table) { - if (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE) { - return obj_transition_too_complex(RCLASS_WRITABLE_ENSURE_FIELDS_OBJ(obj), table); - } - RUBY_ASSERT(!rb_shape_obj_too_complex_p(obj)); shape_id_t shape_id = rb_shape_transition_complex(obj); @@ -1495,11 +1497,12 @@ obj_transition_too_complex(VALUE obj, st_table *table) break; case T_CLASS: case T_MODULE: - rb_bug("Unreachable"); + case T_IMEMO: + UNREACHABLE; break; default: { - VALUE fields_obj = rb_imemo_fields_new_complex_tbl(rb_obj_class(obj), table); + VALUE fields_obj = rb_imemo_fields_new_complex_tbl(obj, table); RBASIC_SET_SHAPE_ID(fields_obj, shape_id); rb_obj_replace_fields(obj, fields_obj); } @@ -1542,124 +1545,102 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) rb_check_frozen(obj); VALUE val = undef; - if (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE) { - IVAR_ACCESSOR_SHOULD_BE_MAIN_RACTOR(id); - - VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(obj); - if (fields_obj) { - if (rb_multi_ractor_p()) { - fields_obj = rb_imemo_fields_clone(fields_obj); - val = rb_ivar_delete(fields_obj, id, undef); - RCLASS_WRITABLE_SET_FIELDS_OBJ(obj, fields_obj); - } - else { - val = rb_ivar_delete(fields_obj, id, undef); - } - // TODO: What should we set as the T_CLASS shape_id? - // In most case we can replicate the single `fields_obj` shape - // but in namespaced case? Perhaps INVALID_SHAPE_ID? - RBASIC_SET_SHAPE_ID(obj, RBASIC_SHAPE_ID(fields_obj)); - } - return val; - } - - shape_id_t old_shape_id = rb_obj_shape_id(obj); - if (rb_shape_too_complex_p(old_shape_id)) { - goto too_complex; - } - - shape_id_t removed_shape_id = 0; - shape_id_t next_shape_id = rb_shape_transition_remove_ivar(obj, id, &removed_shape_id); - - if (next_shape_id == old_shape_id) { - return undef; - } - - if (UNLIKELY(rb_shape_too_complex_p(next_shape_id))) { - rb_evict_fields_to_hash(obj); - goto too_complex; - } - - RUBY_ASSERT(RSHAPE_LEN(next_shape_id) == RSHAPE_LEN(old_shape_id) - 1); + VALUE fields_obj; + bool concurrent = false; + int type = BUILTIN_TYPE(obj); - VALUE *fields; - switch(BUILTIN_TYPE(obj)) { + switch(type) { case T_CLASS: case T_MODULE: - rb_bug("Unreachable"); - break; - case T_IMEMO: - RUBY_ASSERT(IMEMO_TYPE_P(obj, imemo_fields)); - fields = rb_imemo_fields_ptr(obj); + 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 = ROBJECT_FIELDS(obj); + fields_obj = obj; break; default: { - VALUE fields_obj = rb_obj_fields(obj, id); - fields = rb_imemo_fields_ptr(fields_obj); + fields_obj = rb_obj_fields(obj, id); break; } } - RUBY_ASSERT(removed_shape_id != INVALID_SHAPE_ID); + if (!fields_obj) { + return undef; + } - attr_index_t removed_index = RSHAPE_INDEX(removed_shape_id); - val = fields[removed_index]; + const VALUE original_fields_obj = fields_obj; + if (concurrent) { + fields_obj = rb_imemo_fields_clone(fields_obj); + } - attr_index_t new_fields_count = RSHAPE_LEN(next_shape_id); - if (new_fields_count) { - size_t trailing_fields = new_fields_count - removed_index; + shape_id_t old_shape_id = RBASIC_SHAPE_ID(fields_obj); + shape_id_t removed_shape_id; + shape_id_t next_shape_id = rb_shape_transition_remove_ivar(fields_obj, id, &removed_shape_id); - MEMMOVE(&fields[removed_index], &fields[removed_index + 1], VALUE, trailing_fields); + if (UNLIKELY(rb_shape_too_complex_p(next_shape_id))) { + if (UNLIKELY(!rb_shape_too_complex_p(old_shape_id))) { + if (type == T_OBJECT) { + rb_evict_fields_to_hash(obj); + } + else { + fields_obj = imemo_fields_complex_from_obj(obj, fields_obj, next_shape_id); + } + } + st_data_t key = id; + if (!st_delete(rb_imemo_fields_complex_tbl(fields_obj), &key, (st_data_t *)&val)) { + val = undef; + } } else { - rb_free_generic_ivar(obj); - } + if (next_shape_id == old_shape_id) { + return undef; + } - if (RB_TYPE_P(obj, T_OBJECT) && - FL_TEST_RAW(obj, ROBJECT_HEAP) && - rb_obj_embedded_size(new_fields_count) <= rb_gc_obj_slot_size(obj)) { - // Re-embed objects when instances become small enough - // This is necessary because YJIT assumes that objects with the same shape - // have the same embeddedness for efficiency (avoid extra checks) - FL_UNSET_RAW(obj, ROBJECT_HEAP); - MEMCPY(ROBJECT_FIELDS(obj), fields, VALUE, new_fields_count); - xfree(fields); - } - RBASIC_SET_SHAPE_ID(obj, next_shape_id); + RUBY_ASSERT(removed_shape_id != INVALID_SHAPE_ID); + RUBY_ASSERT(RSHAPE_LEN(next_shape_id) == RSHAPE_LEN(old_shape_id) - 1); - return val; + VALUE *fields = rb_imemo_fields_ptr(fields_obj); + attr_index_t removed_index = RSHAPE_INDEX(removed_shape_id); + val = fields[removed_index]; -too_complex: - { - st_table *table = NULL; - switch (BUILTIN_TYPE(obj)) { - case T_CLASS: - case T_MODULE: - rb_bug("Unreachable"); - break; + attr_index_t new_fields_count = RSHAPE_LEN(next_shape_id); + if (new_fields_count) { + size_t trailing_fields = new_fields_count - removed_index; - case T_IMEMO: - RUBY_ASSERT(IMEMO_TYPE_P(obj, imemo_fields)); - table = rb_imemo_fields_complex_tbl(obj); - break; + MEMMOVE(&fields[removed_index], &fields[removed_index + 1], VALUE, trailing_fields); + RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); + + if (type == T_OBJECT && FL_TEST_RAW(obj, ROBJECT_HEAP) && rb_obj_embedded_size(new_fields_count) <= rb_gc_obj_slot_size(obj)) { + // Re-embed objects when instances become small enough + // This is necessary because YJIT assumes that objects with the same shape + // have the same embeddedness for efficiency (avoid extra checks) + FL_UNSET_RAW(obj, ROBJECT_HEAP); + MEMCPY(ROBJECT_FIELDS(obj), fields, VALUE, new_fields_count); + xfree(fields); + } + } + else { + fields_obj = 0; + rb_free_generic_ivar(obj); + } + } + RBASIC_SET_SHAPE_ID(obj, next_shape_id); + if (fields_obj != original_fields_obj) { + switch (type) { case T_OBJECT: - table = ROBJECT_FIELDS_HASH(obj); break; - - default: { - VALUE fields_obj = rb_obj_fields(obj, id); - table = rb_imemo_fields_complex_tbl(fields_obj); + case T_CLASS: + case T_MODULE: + RCLASS_WRITABLE_SET_FIELDS_OBJ(obj, fields_obj); + break; + default: + rb_obj_set_fields(obj, fields_obj, id, original_fields_obj); break; - } - } - - if (table) { - if (!st_delete(table, (st_data_t *)&id, (st_data_t *)&val)) { - val = undef; - } } } From 8eb1ed56195d51ddeba430e6cd1b26f62a402b87 Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Wed, 27 Aug 2025 14:56:57 +0100 Subject: [PATCH 06/22] Revert "CI: Drop Ubuntu s390x temporarily." This reverts commit c3c74e0d31c0c7327d2eb2c79b253d6500c6f2c0. --- .github/workflows/ubuntu-ibm.yml | 193 +++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 .github/workflows/ubuntu-ibm.yml diff --git a/.github/workflows/ubuntu-ibm.yml b/.github/workflows/ubuntu-ibm.yml new file mode 100644 index 00000000000000..619b11bb5e20d1 --- /dev/null +++ b/.github/workflows/ubuntu-ibm.yml @@ -0,0 +1,193 @@ +name: Ubuntu IBM +on: + push: + paths-ignore: + - 'doc/**' + - '**/man/*' + - '**.md' + - '**.rdoc' + - '**/.document' + - '.*.yml' + pull_request: + # Do not use paths-ignore for required status checks + # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks + merge_group: + +concurrency: + group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} + cancel-in-progress: ${{ startsWith(github.event_name, 'pull') }} + +permissions: + contents: read + +jobs: + make: + strategy: + matrix: + test_task: [check] + configure: [''] + arch: [''] + os: + # FIXME Comment out ppc64le due to failing tests on GitHub Actions + # ppc64le + # https://bugs.ruby-lang.org/issues/21534 + # - ubuntu-24.04-ppc64le + - ubuntu-24.04-s390x + # The ppc64le/s390x runners work only in the registered repositories. + # They don't work in forked repositories. + # https://github.com/IBM/actionspz/blob/main/docs/FAQ.md#what-about-forked-repos + upstream: + - ${{ github.repository == 'ruby/ruby' }} + exclude: + - os: ubuntu-24.04-ppc64le + upstream: false + - os: ubuntu-24.04-s390x + upstream: false + fail-fast: false + + env: + GITPULLOPTIONS: --no-tags origin ${{ github.ref }} + RUBY_DEBUG: ci + + runs-on: ${{ matrix.os || 'ubuntu-22.04' }} + + if: >- + ${{!(false + || contains(github.event.head_commit.message, '[DOC]') + || contains(github.event.pull_request.title, '[DOC]') + || contains(github.event.pull_request.labels.*.name, 'Documentation') + || (github.event_name == 'push' && github.event.pull_request.user.login == 'dependabot[bot]') + )}} + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + sparse-checkout-cone-mode: false + sparse-checkout: /.github + + - uses: ./.github/actions/setup/ubuntu + with: + arch: ${{ matrix.arch }} + + - uses: ruby/setup-ruby@a9bfc2ecf3dd40734a9418f89a7e9d484c32b990 # v1.248.0 + with: + ruby-version: '3.1' + bundler: none + if: ${{ !endsWith(matrix.os, 'arm') && !endsWith(matrix.os, 'ppc64le') && !endsWith(matrix.os, 's390x') }} + + # Avoid possible test failures with the zlib applying the following patch + # on s390x CPU architecture. + # https://github.com/madler/zlib/pull/410 + - name: Disable DFLTCC + run: echo "DFLTCC=0" >> $GITHUB_ENV + working-directory: + if: ${{ endsWith(matrix.os, 's390x') }} + + # A temporary workaround: Set HOME env to pass the step + # ./.github/actions/setup/directories. + # https://github.com/IBM/actionspz/issues/30 + - name: Set HOME env + run: | + echo "HOME: #{HOME}" + echo "HOME=$(ls -d ~)" >> $GITHUB_ENV + working-directory: + if: ${{ endsWith(matrix.os, 'ppc64le') || endsWith(matrix.os, 's390x') }} + + - uses: ./.github/actions/setup/directories + with: + srcdir: src + builddir: build + makeup: true + clean: true + dummy-files: ${{ matrix.test_task == 'check' }} + # Set fetch-depth: 10 so that Launchable can receive commits information. + fetch-depth: 10 + + - name: Run configure + env: + arch: ${{ matrix.arch }} + configure: ${{ matrix.configure }} + run: >- + $SETARCH ../src/configure -C --disable-install-doc ${configure:-cppflags=-DRUBY_DEBUG} + ${arch:+--target=$arch-$OSTYPE --host=$arch-$OSTYPE} + + - run: $SETARCH make prepare-gems + if: ${{ matrix.test_task == 'test-bundled-gems' }} + + - run: $SETARCH make + + - run: $SETARCH make hello + + - name: runirb + run: | + echo IRB::VERSION | $SETARCH make runirb RUNOPT="-- -f" + + - name: Set test options for skipped tests + run: | + set -x + TESTS="$(echo "${{ matrix.skipped_tests }}" | sed 's| |$$/ -n!/|g;s|^|-n!/|;s|$|$$/|')" + echo "TESTS=${TESTS}" >> $GITHUB_ENV + if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} + + - name: Set up Launchable + id: launchable + uses: ./.github/actions/launchable/setup + with: + os: ${{ matrix.os || 'ubuntu-22.04' }} + test-opts: ${{ matrix.configure }} + launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} + builddir: build + srcdir: src + continue-on-error: true + timeout-minutes: 3 + + # A temporary workaround: Skip user ground id test + # There is a mismatch between the group IDs of "id -g" and C function + # getpwuid(uid_t uid) pw_gid. + # https://github.com/IBM/actionspz/issues/31 + - name: Skip user group id test + run: | + sed -i.orig '/^ it "returns user group id" do/a\ skip' \ + ../src/spec/ruby/library/etc/struct_passwd_spec.rb + diff -u ../src/spec/ruby/library/etc/struct_passwd_spec.rb{.orig,} || : + if: ${{ endsWith(matrix.os, 'ppc64le') || endsWith(matrix.os, 's390x') }} + + - name: make ${{ matrix.test_task }} + run: | + test -n "${LAUNCHABLE_STDOUT}" && exec 1> >(tee "${LAUNCHABLE_STDOUT}") + test -n "${LAUNCHABLE_STDERR}" && exec 2> >(tee "${LAUNCHABLE_STDERR}") + + $SETARCH make -s ${{ matrix.test_task }} \ + ${TESTS:+TESTS="$TESTS"} \ + ${{ !contains(matrix.test_task, 'bundle') && 'RUBYOPT=-w' || '' }} + timeout-minutes: ${{ matrix.timeout || 40 }} + env: + RUBY_TESTOPTS: '-q --tty=no' + TEST_BUNDLED_GEMS_ALLOW_FAILURES: '' + PRECHECK_BUNDLED_GEMS: 'no' + LAUNCHABLE_STDOUT: ${{ steps.launchable.outputs.stdout_report_path }} + LAUNCHABLE_STDERR: ${{ steps.launchable.outputs.stderr_report_path }} + + - name: make skipped tests + run: | + $SETARCH make -s test-all TESTS="${TESTS//-n!\//-n/}" + env: + GNUMAKEFLAGS: '' + RUBY_TESTOPTS: '-v --tty=no' + if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} + continue-on-error: ${{ matrix.continue-on-skipped_tests || false }} + + - name: test-pc + run: | + DESTDIR=${RUNNER_TEMP-${TMPDIR-/tmp}}/installed + $SETARCH make test-pc "DESTDIR=$DESTDIR" + + - uses: ./.github/actions/slack + with: + label: ${{ matrix.test_task }} ${{ matrix.configure }}${{ matrix.arch }} + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot + if: ${{ failure() }} + +defaults: + run: + working-directory: build From dd09d8987c9988b48d43352b922c0df06bc9a47f Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Wed, 27 Aug 2025 15:06:46 +0100 Subject: [PATCH 07/22] CI: ubuntu-ibm.yml: Refactor * Remove logic that was used for the ubuntu.yml, but not used for ubuntu-ibm.yml. * Add a dummy Ubuntu x86_64 case to make this CI pass on fork repositories. This case only runs on fork repositories. --- .github/workflows/ubuntu-ibm.yml | 34 ++++++++++++++------------------ 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ubuntu-ibm.yml b/.github/workflows/ubuntu-ibm.yml index 619b11bb5e20d1..8b831bcd25f298 100644 --- a/.github/workflows/ubuntu-ibm.yml +++ b/.github/workflows/ubuntu-ibm.yml @@ -26,13 +26,14 @@ jobs: matrix: test_task: [check] configure: [''] - arch: [''] os: # FIXME Comment out ppc64le due to failing tests on GitHub Actions # ppc64le # https://bugs.ruby-lang.org/issues/21534 # - ubuntu-24.04-ppc64le - ubuntu-24.04-s390x + # Add a x86_64 case to make this CI pass on fork repositories. + - ubuntu-24.04 # The ppc64le/s390x runners work only in the registered repositories. # They don't work in forked repositories. # https://github.com/IBM/actionspz/blob/main/docs/FAQ.md#what-about-forked-repos @@ -43,13 +44,15 @@ jobs: upstream: false - os: ubuntu-24.04-s390x upstream: false + - os: ubuntu-24.04 + upstream: true fail-fast: false env: GITPULLOPTIONS: --no-tags origin ${{ github.ref }} RUBY_DEBUG: ci - runs-on: ${{ matrix.os || 'ubuntu-22.04' }} + runs-on: ${{ matrix.os }} if: >- ${{!(false @@ -66,14 +69,12 @@ jobs: sparse-checkout: /.github - uses: ./.github/actions/setup/ubuntu - with: - arch: ${{ matrix.arch }} - uses: ruby/setup-ruby@a9bfc2ecf3dd40734a9418f89a7e9d484c32b990 # v1.248.0 with: ruby-version: '3.1' bundler: none - if: ${{ !endsWith(matrix.os, 'arm') && !endsWith(matrix.os, 'ppc64le') && !endsWith(matrix.os, 's390x') }} + if: ${{ !endsWith(matrix.os, 'ppc64le') && !endsWith(matrix.os, 's390x') }} # Avoid possible test failures with the zlib applying the following patch # on s390x CPU architecture. @@ -105,22 +106,17 @@ jobs: - name: Run configure env: - arch: ${{ matrix.arch }} configure: ${{ matrix.configure }} run: >- - $SETARCH ../src/configure -C --disable-install-doc ${configure:-cppflags=-DRUBY_DEBUG} - ${arch:+--target=$arch-$OSTYPE --host=$arch-$OSTYPE} - - - run: $SETARCH make prepare-gems - if: ${{ matrix.test_task == 'test-bundled-gems' }} + ../src/configure -C --disable-install-doc ${configure:-cppflags=-DRUBY_DEBUG} - - run: $SETARCH make + - run: make - - run: $SETARCH make hello + - run: make hello - name: runirb run: | - echo IRB::VERSION | $SETARCH make runirb RUNOPT="-- -f" + echo IRB::VERSION | make runirb RUNOPT="-- -f" - name: Set test options for skipped tests run: | @@ -133,7 +129,7 @@ jobs: id: launchable uses: ./.github/actions/launchable/setup with: - os: ${{ matrix.os || 'ubuntu-22.04' }} + os: ${{ matrix.os }} test-opts: ${{ matrix.configure }} launchable-token: ${{ secrets.LAUNCHABLE_TOKEN }} builddir: build @@ -157,7 +153,7 @@ jobs: test -n "${LAUNCHABLE_STDOUT}" && exec 1> >(tee "${LAUNCHABLE_STDOUT}") test -n "${LAUNCHABLE_STDERR}" && exec 2> >(tee "${LAUNCHABLE_STDERR}") - $SETARCH make -s ${{ matrix.test_task }} \ + make -s ${{ matrix.test_task }} \ ${TESTS:+TESTS="$TESTS"} \ ${{ !contains(matrix.test_task, 'bundle') && 'RUBYOPT=-w' || '' }} timeout-minutes: ${{ matrix.timeout || 40 }} @@ -170,7 +166,7 @@ jobs: - name: make skipped tests run: | - $SETARCH make -s test-all TESTS="${TESTS//-n!\//-n/}" + make -s test-all TESTS="${TESTS//-n!\//-n/}" env: GNUMAKEFLAGS: '' RUBY_TESTOPTS: '-v --tty=no' @@ -180,11 +176,11 @@ jobs: - name: test-pc run: | DESTDIR=${RUNNER_TEMP-${TMPDIR-/tmp}}/installed - $SETARCH make test-pc "DESTDIR=$DESTDIR" + make test-pc "DESTDIR=$DESTDIR" - uses: ./.github/actions/slack with: - label: ${{ matrix.test_task }} ${{ matrix.configure }}${{ matrix.arch }} + label: ${{ matrix.test_task }} ${{ matrix.configure }} SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} # ruby-lang slack: ruby/simpler-alerts-bot if: ${{ failure() }} From 4fc0db7389d2c93eebf232e802769a38702eea8e Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Wed, 27 Aug 2025 18:03:35 +0100 Subject: [PATCH 08/22] CI: ubuntu-ibm.yml: Add GitHub Actions ppc64le case Note that the default configure option `./configure cppflags=-DRUBY_DEBUG` with the default optimization level `-O3`, causes the following Ractor test and other tests failing. So, we don't set the option in ppc64le case. ``` $ make btest BTESTS=bootstraptest/test_ractor.rb ``` See https://bugs.ruby-lang.org/issues/21534 for details. --- .github/workflows/ubuntu-ibm.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ubuntu-ibm.yml b/.github/workflows/ubuntu-ibm.yml index 8b831bcd25f298..f5d93365c28685 100644 --- a/.github/workflows/ubuntu-ibm.yml +++ b/.github/workflows/ubuntu-ibm.yml @@ -27,10 +27,7 @@ jobs: test_task: [check] configure: [''] os: - # FIXME Comment out ppc64le due to failing tests on GitHub Actions - # ppc64le - # https://bugs.ruby-lang.org/issues/21534 - # - ubuntu-24.04-ppc64le + - ubuntu-24.04-ppc64le - ubuntu-24.04-s390x # Add a x86_64 case to make this CI pass on fork repositories. - ubuntu-24.04 @@ -107,8 +104,14 @@ jobs: - name: Run configure env: configure: ${{ matrix.configure }} - run: >- - ../src/configure -C --disable-install-doc ${configure:-cppflags=-DRUBY_DEBUG} + # Don't set cppflags=-DRUBY_DEBUG on ppc64le, due to some Ractor tests + # failing in the case. + # https://bugs.ruby-lang.org/issues/21534 + run: | + if [ "$(uname -m)" != "ppc64le" ]; then + configure="${configure:-cppflags=-DRUBY_DEBUG}" + fi + ../src/configure -C --disable-install-doc ${configure} - run: make From c4c93a0751849a3e8dc738a98cc690df99155fa2 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 28 Aug 2025 08:58:23 -0700 Subject: [PATCH 09/22] ZJIT: Refactor stats implementations (#14378) * ZJIT: Refactor stats implementations * s/num_send_dynamic/dynamic_send_count/ --- test/ruby/test_zjit.rb | 17 ++++++++- zjit.rb | 15 ++------ zjit/src/backend/lir.rs | 37 +++++++++--------- zjit/src/codegen.rs | 18 ++++----- zjit/src/hir.rs | 4 +- zjit/src/stats.rs | 84 ++++++++++++++++++++++++++--------------- 6 files changed, 103 insertions(+), 72 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 9296cd35227429..52009d9d0ab520 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -1485,7 +1485,7 @@ def test_require_rubygems_with_auto_compact }, call_threshold: 2 end - def test_stats + def test_stats_availability assert_runs '[true, true]', %q{ def test = 1 test @@ -1496,6 +1496,21 @@ def test = 1 }, stats: true end + def test_stats_consistency + assert_runs '[]', %q{ + def test = 1 + test # increment some counters + + RubyVM::ZJIT.stats.to_a.filter_map do |key, value| + # The value may be incremented, but the class should stay the same + other_value = RubyVM::ZJIT.stats(key) + if value.class != other_value.class + [key, value, other_value] + end + end + }, stats: true + end + def test_zjit_option_uses_array_each_in_ruby omit 'ZJIT wrongly compiles Array#each, so it is disabled for now' assert_runs '""', %q{ diff --git a/zjit.rb b/zjit.rb index 86d6643df21dd0..41ce371e7e9da9 100644 --- a/zjit.rb +++ b/zjit.rb @@ -25,16 +25,8 @@ def stats_enabled? end # Return ZJIT statistics as a Hash - def stats(key = nil) - stats = Primitive.rb_zjit_stats(key) - return stats if stats.nil? || !key.nil? - - if stats.key?(:vm_insn_count) && stats.key?(:zjit_insn_count) - stats[:total_insn_count] = stats[:vm_insn_count] + stats[:zjit_insn_count] - stats[:ratio_in_zjit] = 100.0 * stats[:zjit_insn_count] / stats[:total_insn_count] - end - - stats + def stats(target_key = nil) + Primitive.rb_zjit_stats(target_key) end # Get the summary of ZJIT statistics as a String @@ -44,6 +36,8 @@ def stats_string print_counters_with_prefix(prefix: 'failed_', prompt: 'compilation failure reasons', buf:, stats:) print_counters([ + :dynamic_send_count, + :compiled_iseq_count, :compilation_failure, @@ -56,7 +50,6 @@ def stats_string :total_insn_count, :vm_insn_count, :zjit_insn_count, - :zjit_dynamic_dispatch, :ratio_in_zjit, ], buf:, stats:) print_counters_with_prefix(prefix: 'exit_', prompt: 'side exit reasons', buf:, stats:, limit: 20) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 0639176fd63f8b..b372acccea21b3 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -6,7 +6,7 @@ use crate::cruby::{Qundef, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_ use crate::hir::SideExitReason; use crate::options::{debug, get_option}; use crate::cruby::VALUE; -use crate::stats::exit_counter_ptr; +use crate::stats::{exit_counter_ptr, specific_exit_counter_ptr}; use crate::virtualmem::CodePtr; use crate::asm::{CodeBlock, Label}; @@ -1593,29 +1593,15 @@ impl Assembler let cfp_sp = Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SP); self.store(cfp_sp, SCRATCH_OPND); + // Using C_RET_OPND as an additional scratch register, which is no longer used if get_option!(stats) { asm_comment!(self, "increment an exit counter"); self.load_into(SCRATCH_OPND, Opnd::const_ptr(exit_counter_ptr(pc))); - let counter_opnd = if cfg!(target_arch = "aarch64") { // See arm64_split() - // Using C_CRET_OPND since arm64_emit uses both SCRATCH0 and SCRATCH1 for IncrCounter. - self.lea_into(C_RET_OPND, Opnd::mem(64, SCRATCH_OPND, 0)); - C_RET_OPND - } else { // x86_emit expects Opnd::Mem - Opnd::mem(64, SCRATCH_OPND, 0) - }; - self.incr_counter(counter_opnd, 1.into()); + self.incr_counter_with_reg(Opnd::mem(64, SCRATCH_OPND, 0), 1.into(), C_RET_OPND); asm_comment!(self, "increment a specific exit counter"); - let counter = crate::stats::side_exit_reason_counter(reason); - self.load_into(SCRATCH_OPND, Opnd::const_ptr(crate::stats::counter_ptr(counter))); - let counter_opnd = if cfg!(target_arch = "aarch64") { // See arm64_split() - // Using C_CRET_OPND since arm64_emit uses both SCRATCH0 and SCRATCH1 for IncrCounter. - self.lea_into(C_RET_OPND, Opnd::mem(64, SCRATCH_OPND, 0)); - C_RET_OPND - } else { // x86_emit expects Opnd::Mem - Opnd::mem(64, SCRATCH_OPND, 0) - }; - self.incr_counter(counter_opnd, 1.into()); + self.load_into(SCRATCH_OPND, Opnd::const_ptr(specific_exit_counter_ptr(reason))); + self.incr_counter_with_reg(Opnd::mem(64, SCRATCH_OPND, 0), 1.into(), C_RET_OPND); } asm_comment!(self, "exit to the interpreter"); @@ -1799,6 +1785,19 @@ impl Assembler { self.push_insn(Insn::IncrCounter { mem, value }); } + /// incr_counter() but uses a specific register to split Insn::Lea + pub fn incr_counter_with_reg(&mut self, mem: Opnd, value: Opnd, reg: Opnd) { + assert!(matches!(reg, Opnd::Reg(_)), "incr_counter_with_reg should take a register, got: {reg:?}"); + let counter_opnd = if cfg!(target_arch = "aarch64") { // See arm64_split() + assert_ne!(reg, SCRATCH_OPND, "SCRATCH_REG should be reserved for IncrCounter"); + self.lea_into(reg, mem); + reg + } else { // x86_emit() expects Opnd::Mem + mem + }; + self.incr_counter(counter_opnd, value); + } + pub fn jbe(&mut self, target: Target) { self.push_insn(Insn::Jbe(target)); } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index fd804e5a426cd5..ba28af73b14fdb 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -858,9 +858,7 @@ fn gen_send_without_block( cd: *const rb_call_data, state: &FrameState, ) -> lir::Opnd { - if get_option!(stats) { - gen_incr_counter(asm, Counter::zjit_dynamic_dispatch); - } + gen_incr_counter(asm, Counter::dynamic_send_count); // Note that it's incorrect to use this frame state to side exit because // the state might not be on the boundary of an interpreter instruction. @@ -1223,14 +1221,16 @@ fn gen_guard_bit_equals(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val } -/// Generate code that increments a counter in ZJIT stats +/// Generate code that increments a counter if --zjit-stats fn gen_incr_counter(asm: &mut Assembler, counter: Counter) { - let ptr = counter_ptr(counter); - let ptr_reg = asm.load(Opnd::const_ptr(ptr as *const u8)); - let counter_opnd = Opnd::mem(64, ptr_reg, 0); + if get_option!(stats) { + let ptr = counter_ptr(counter); + let ptr_reg = asm.load(Opnd::const_ptr(ptr as *const u8)); + let counter_opnd = Opnd::mem(64, ptr_reg, 0); - // Increment and store the updated value - asm.incr_counter(counter_opnd, Opnd::UImm(1)); + // Increment and store the updated value + asm.incr_counter(counter_opnd, Opnd::UImm(1)); + } } /// Save the incremented PC on the CFP. diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 039ba6de5b010a..d1f5a3af8485b1 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -431,7 +431,9 @@ pub enum SideExitReason { UnknownNewarraySend(vm_opt_newarray_send_type), UnknownCallType, UnknownOpcode(u32), + UnknownSpecialVariable(u64), UnhandledInstruction(InsnId), + UnhandledDefinedType(usize), FixnumAddOverflow, FixnumSubOverflow, FixnumMultOverflow, @@ -440,8 +442,6 @@ pub enum SideExitReason { PatchPoint(Invariant), CalleeSideExit, ObjToStringFallback, - UnknownSpecialVariable(u64), - UnhandledDefinedType(usize), Interrupt, } diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 10692d83f57428..5d0a66ff2c263f 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -73,7 +73,7 @@ make_counters! { zjit_insn_count, // The number of times we do a dynamic dispatch from JIT code - zjit_dynamic_dispatch, + dynamic_send_count, // failed_: Compilation failure reasons failed_iseq_stack_too_large, @@ -89,7 +89,9 @@ make_counters! { specific_exit_unknown_newarray_send, specific_exit_unknown_call_type, specific_exit_unknown_opcode, + specific_exit_unknown_special_variable, specific_exit_unhandled_instruction, + specific_exit_unhandled_defined_type, specific_exit_fixnum_add_overflow, specific_exit_fixnum_sub_overflow, specific_exit_fixnum_mult_overflow, @@ -98,8 +100,6 @@ make_counters! { specific_exit_patchpoint, specific_exit_callee_side_exit, specific_exit_obj_to_string_fallback, - specific_exit_unknown_special_variable, - specific_exit_unhandled_defined_type, specific_exit_interrupt, } @@ -127,25 +127,27 @@ pub fn exit_counter_ptr(pc: *const VALUE) -> *mut u64 { unsafe { exit_counters.get_unchecked_mut(opcode as usize) } } -pub fn side_exit_reason_counter(reason: crate::hir::SideExitReason) -> Counter { - use crate::hir::SideExitReason; - match reason { - SideExitReason::UnknownNewarraySend(_) => Counter::specific_exit_unknown_newarray_send, - SideExitReason::UnknownCallType => Counter::specific_exit_unknown_call_type, - SideExitReason::UnknownOpcode(_) => Counter::specific_exit_unknown_opcode, - SideExitReason::UnhandledInstruction(_) => Counter::specific_exit_unhandled_instruction, - SideExitReason::FixnumAddOverflow => Counter::specific_exit_fixnum_add_overflow, - SideExitReason::FixnumSubOverflow => Counter::specific_exit_fixnum_sub_overflow, - SideExitReason::FixnumMultOverflow => Counter::specific_exit_fixnum_mult_overflow, - SideExitReason::GuardType(_) => Counter::specific_exit_guard_type_failure, - SideExitReason::GuardBitEquals(_) => Counter::specific_exit_guard_bit_equals_failure, - SideExitReason::PatchPoint(_) => Counter::specific_exit_patchpoint, - SideExitReason::CalleeSideExit => Counter::specific_exit_callee_side_exit, - SideExitReason::ObjToStringFallback => Counter::specific_exit_obj_to_string_fallback, - SideExitReason::UnknownSpecialVariable(_) => Counter::specific_exit_unknown_special_variable, - SideExitReason::UnhandledDefinedType(_) => Counter::specific_exit_unhandled_defined_type, - SideExitReason::Interrupt => Counter::specific_exit_interrupt, - } +pub fn specific_exit_counter_ptr(reason: crate::hir::SideExitReason) -> *mut u64 { + use crate::hir::SideExitReason::*; + use crate::stats::Counter::*; + let counter = match reason { + UnknownNewarraySend(_) => specific_exit_unknown_newarray_send, + UnknownCallType => specific_exit_unknown_call_type, + UnknownOpcode(_) => specific_exit_unknown_opcode, + UnknownSpecialVariable(_) => specific_exit_unknown_special_variable, + UnhandledInstruction(_) => specific_exit_unhandled_instruction, + UnhandledDefinedType(_) => specific_exit_unhandled_defined_type, + FixnumAddOverflow => specific_exit_fixnum_add_overflow, + FixnumSubOverflow => specific_exit_fixnum_sub_overflow, + FixnumMultOverflow => specific_exit_fixnum_mult_overflow, + GuardType(_) => specific_exit_guard_type_failure, + GuardBitEquals(_) => specific_exit_guard_bit_equals_failure, + PatchPoint(_) => specific_exit_patchpoint, + CalleeSideExit => specific_exit_callee_side_exit, + ObjToStringFallback => specific_exit_obj_to_string_fallback, + Interrupt => specific_exit_interrupt, + }; + counter_ptr(counter) } /// Return a Hash object that contains ZJIT statistics @@ -158,13 +160,24 @@ pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE, target_key: VALUE) -> macro_rules! set_stat { ($hash:ident, $key:expr, $value:expr) => { let key = rust_str_to_sym($key); - // Evaluate $value only when it's needed if key == target_key { - return VALUE::fixnum_from_usize($value as usize); + return $value; } else if $hash != Qnil { #[allow(unused_unsafe)] - unsafe { rb_hash_aset($hash, key, VALUE::fixnum_from_usize($value as usize)); } + unsafe { rb_hash_aset($hash, key, $value); } } + }; + } + + macro_rules! set_stat_usize { + ($hash:ident, $key:expr, $value:expr) => { + set_stat!($hash, $key, VALUE::fixnum_from_usize($value as usize)) + } + } + + macro_rules! set_stat_f64 { + ($hash:ident, $key:expr, $value:expr) => { + set_stat!($hash, $key, unsafe { rb_float_new($value) }) } } @@ -177,14 +190,14 @@ pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE, target_key: VALUE) -> // If not --zjit-stats, set only default counters if !get_option!(stats) { for &counter in DEFAULT_COUNTERS { - set_stat!(hash, &counter.name(), unsafe { *counter_ptr(counter) }); + set_stat_usize!(hash, &counter.name(), unsafe { *counter_ptr(counter) }); } return hash; } // Set all counters for --zjit-stats for &counter in ALL_COUNTERS { - set_stat!(hash, &counter.name(), unsafe { *counter_ptr(counter) }); + set_stat_usize!(hash, &counter.name(), unsafe { *counter_ptr(counter) }); } // Set side exit stats @@ -195,12 +208,23 @@ pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE, target_key: VALUE) -> let key_string = "exit_".to_owned() + &op_name; let count = exit_counters[op_idx]; side_exit_count += count; - set_stat!(hash, &key_string, count); + set_stat_usize!(hash, &key_string, count); } - set_stat!(hash, "side_exit_count", side_exit_count); + set_stat_usize!(hash, "side_exit_count", side_exit_count); + + // Share compilation_failure among both prefixes for side-exit stats + let counters = ZJITState::get_counters(); + set_stat_usize!(hash, "specific_exit_compilation_failure", counters.exit_compilation_failure); + // Only ZJIT_STATS builds support rb_vm_insn_count if unsafe { rb_vm_insn_count } > 0 { - set_stat!(hash, "vm_insn_count", unsafe { rb_vm_insn_count }); + let vm_insn_count = unsafe { rb_vm_insn_count }; + set_stat_usize!(hash, "vm_insn_count", vm_insn_count); + + let total_insn_count = vm_insn_count + counters.zjit_insn_count; + set_stat_usize!(hash, "total_insn_count", total_insn_count); + + set_stat_f64!(hash, "ratio_in_yjit", 100.0 * vm_insn_count as f64 / total_insn_count as f64); } hash From 737ffd33170de56f739ead96118358d72b738aad Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 28 Aug 2025 17:31:26 +0100 Subject: [PATCH 10/22] ZJIT: Add Ractor mode PatchPoint for ivar get/set (#14375) * ZJIT: Add Ractor mode PatchPoint for ivar get/set * ZJIT: Only add single ractor patchpoint to class/module receivers * ZJIT: Improve Ractor mode patch point comments Co-authored-by: Takashi Kokubun --------- Co-authored-by: Takashi Kokubun --- test/ruby/test_zjit.rb | 79 ++++++++++++++++++++++++++++++++++++++++++ zjit/src/hir.rs | 56 ++++++++++++++++++++++-------- 2 files changed, 120 insertions(+), 15 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 52009d9d0ab520..692dfb486cc2de 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -1919,6 +1919,85 @@ def write(hash, key) } end + def test_ivar_attr_reader_optimization_with_multi_ractor_mode + assert_compiles '42', %q{ + class Foo + class << self + attr_accessor :bar + + def get_bar + bar + rescue Ractor::IsolationError + 42 + end + end + end + + Foo.bar = [] # needs to be a ractor unshareable object + + def test + Foo.get_bar + end + + test + test + + Ractor.new { test }.value + }, call_threshold: 2 + end + + def test_ivar_get_with_multi_ractor_mode + assert_compiles '42', %q{ + class Foo + def self.set_bar + @bar = [] # needs to be a ractor unshareable object + end + + def self.bar + @bar + rescue Ractor::IsolationError + 42 + end + end + + Foo.set_bar + + def test + Foo.bar + end + + test + test + + Ractor.new { test }.value + } + end + + def test_ivar_set_with_multi_ractor_mode + assert_compiles '42', %q{ + class Foo + def self.bar + _foo = 1 + _bar = 2 + begin + @bar = _foo + _bar + rescue Ractor::IsolationError + 42 + end + end + end + + def test + Foo.bar + end + + test + test + + Ractor.new { test }.value + } + end + private # Assert that every method call in `test_script` can be compiled by ZJIT diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index d1f5a3af8485b1..ee90032ee870c1 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1671,6 +1671,15 @@ impl Function { self_val = self.push_insn(block, Insn::GuardType { val: self_val, guard_type: Type::from_profiled_type(profiled_type), state }); } let id = unsafe { get_cme_def_body_attr_id(cme) }; + + // Check if we're accessing ivars of a Class or Module object as they require single-ractor mode. + // We omit gen_prepare_non_leaf_call on gen_getivar, so it's unsafe to raise for multi-ractor mode. + if unsafe { rb_zjit_singleton_class_p(klass) } { + let attached = unsafe { rb_class_attached_object(klass) }; + if unsafe { RB_TYPE_P(attached, RUBY_T_CLASS) || RB_TYPE_P(attached, RUBY_T_MODULE) } { + self.push_insn(block, Insn::PatchPoint { invariant: Invariant::SingleRactorMode, state }); + } + } let getivar = self.push_insn(block, Insn::GetIvar { self_val, id, state }); self.make_equal_to(insn_id, getivar); } else { @@ -3332,6 +3341,9 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let id = ID(get_arg(pc, 0).as_u64()); // ic is in arg 1 let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + // Assume single-Ractor mode to omit gen_prepare_non_leaf_call on gen_getivar + // TODO: We only really need this if self_val is a class/module + fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::SingleRactorMode, state: exit_id }); let result = fun.push_insn(block, Insn::GetIvar { self_val: self_param, id, state: exit_id }); state.stack_push(result); } @@ -3339,6 +3351,9 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let id = ID(get_arg(pc, 0).as_u64()); // ic is in arg 1 let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state }); + // Assume single-Ractor mode to omit gen_prepare_non_leaf_call on gen_setivar + // TODO: We only really need this if self_val is a class/module + fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::SingleRactorMode, state: exit_id }); let val = state.stack_pop()?; fun.push_insn(block, Insn::SetIvar { self_val: self_param, id, val, state: exit_id }); } @@ -5053,9 +5068,10 @@ mod tests { assert_snapshot!(hir_string("test"), @r" fn test@:2: bb0(v0:BasicObject): - v3:BasicObject = GetIvar v0, :@foo + PatchPoint SingleRactorMode + v4:BasicObject = GetIvar v0, :@foo CheckInterrupts - Return v3 + Return v4 "); } @@ -5070,6 +5086,7 @@ mod tests { fn test@:2: bb0(v0:BasicObject): v2:Fixnum[1] = Const Value(1) + PatchPoint SingleRactorMode SetIvar v0, :@foo, v2 CheckInterrupts Return v2 @@ -5345,12 +5362,15 @@ mod tests { v1:NilClass = Const Value(nil) v2:NilClass = Const Value(nil) v3:NilClass = Const Value(nil) - v6:BasicObject = GetIvar v0, :@a - v8:BasicObject = GetIvar v0, :@b - v10:BasicObject = GetIvar v0, :@c - v12:ArrayExact = NewArray v6, v8, v10 + PatchPoint SingleRactorMode + v7:BasicObject = GetIvar v0, :@a + PatchPoint SingleRactorMode + v10:BasicObject = GetIvar v0, :@b + PatchPoint SingleRactorMode + v13:BasicObject = GetIvar v0, :@c + v15:ArrayExact = NewArray v7, v10, v13 CheckInterrupts - Return v12 + Return v15 "); assert_contains_opcode("reverse_even", YARVINSN_opt_reverse); assert_snapshot!(hir_string("reverse_even"), @r" @@ -5360,13 +5380,17 @@ mod tests { v2:NilClass = Const Value(nil) v3:NilClass = Const Value(nil) v4:NilClass = Const Value(nil) - v7:BasicObject = GetIvar v0, :@a - v9:BasicObject = GetIvar v0, :@b - v11:BasicObject = GetIvar v0, :@c - v13:BasicObject = GetIvar v0, :@d - v15:ArrayExact = NewArray v7, v9, v11, v13 + PatchPoint SingleRactorMode + v8:BasicObject = GetIvar v0, :@a + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v0, :@b + PatchPoint SingleRactorMode + v14:BasicObject = GetIvar v0, :@c + PatchPoint SingleRactorMode + v17:BasicObject = GetIvar v0, :@d + v19:ArrayExact = NewArray v8, v11, v14, v17 CheckInterrupts - Return v15 + Return v19 "); } @@ -7454,9 +7478,10 @@ mod opt_tests { assert_snapshot!(hir_string("test"), @r" fn test@:2: bb0(v0:BasicObject): - v3:BasicObject = GetIvar v0, :@foo + PatchPoint SingleRactorMode + v4:BasicObject = GetIvar v0, :@foo CheckInterrupts - Return v3 + Return v4 "); } @@ -7469,6 +7494,7 @@ mod opt_tests { fn test@:2: bb0(v0:BasicObject): v2:Fixnum[1] = Const Value(1) + PatchPoint SingleRactorMode SetIvar v0, :@foo, v2 CheckInterrupts Return v2 From b95700f17c2779f182eb7786c4b61156463e3d3a Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 28 Aug 2025 11:19:55 -0400 Subject: [PATCH 11/22] ZJIT: Track object embedded bit This lets us know where to look for an ivar: in the object or indirect elsewhere in the heap. --- zjit/src/profile.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index b311f0ba94edf8..075a0793384caf 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -112,11 +112,14 @@ struct Flags(u32); impl Flags { const NONE: u32 = 0; const IS_IMMEDIATE: u32 = 1 << 0; + /// Object is embedded and the ivar index lands within the object + const IS_EMBEDDED: u32 = 1 << 1; pub fn none() -> Self { Self(Self::NONE) } pub fn immediate() -> Self { Self(Self::IS_IMMEDIATE) } pub fn is_immediate(self) -> bool { (self.0 & Self::IS_IMMEDIATE) != 0 } + pub fn is_embedded(self) -> bool { (self.0 & Self::IS_EMBEDDED) != 0 } } /// opt_send_without_block/opt_plus/... should store: @@ -174,7 +177,11 @@ impl ProfiledType { shape: INVALID_SHAPE_ID, flags: Flags::immediate() }; } - Self { class: obj.class_of(), shape: obj.shape_id_of(), flags: Flags::none() } + let mut flags = Flags::none(); + if obj.embedded_p() { + flags.0 |= Flags::IS_EMBEDDED; + } + Self { class: obj.class_of(), shape: obj.shape_id_of(), flags } } pub fn empty() -> Self { From 11f115b0d233215f2575333da0e9351dcfc4a11f Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 28 Aug 2025 11:30:31 -0400 Subject: [PATCH 12/22] ZJIT: Track if object is a T_OBJECT We will (for now) only cache ivar reads from T_OBJECTs. --- zjit/src/profile.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index 075a0793384caf..9da48a0705be64 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -114,12 +114,15 @@ impl Flags { const IS_IMMEDIATE: u32 = 1 << 0; /// Object is embedded and the ivar index lands within the object const IS_EMBEDDED: u32 = 1 << 1; + /// Object is a T_OBJECT + const IS_T_OBJECT: u32 = 1 << 2; pub fn none() -> Self { Self(Self::NONE) } pub fn immediate() -> Self { Self(Self::IS_IMMEDIATE) } pub fn is_immediate(self) -> bool { (self.0 & Self::IS_IMMEDIATE) != 0 } pub fn is_embedded(self) -> bool { (self.0 & Self::IS_EMBEDDED) != 0 } + pub fn is_t_object(self) -> bool { (self.0 & Self::IS_T_OBJECT) != 0 } } /// opt_send_without_block/opt_plus/... should store: @@ -181,6 +184,9 @@ impl ProfiledType { if obj.embedded_p() { flags.0 |= Flags::IS_EMBEDDED; } + if unsafe { RB_TYPE_P(obj, RUBY_T_OBJECT) } { + flags.0 |= Flags::IS_T_OBJECT; + } Self { class: obj.class_of(), shape: obj.shape_id_of(), flags } } From ca0ef794971f4fddf92c4652dee17ada63d52dbe Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 27 Aug 2025 22:16:10 -0400 Subject: [PATCH 13/22] ZJIT: Generate code for HashDup --- test/ruby/test_zjit.rb | 10 ++++++++++ zjit/src/codegen.rs | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 692dfb486cc2de..7c29fdeb193256 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -99,6 +99,16 @@ def test }, insns: [:intern] end + def test_duphash + assert_compiles '{a: 1}', %q{ + def test + {a: 1} + end + + test + }, insns: [:duphash] + end + def test_setglobal_with_trace_var_exception assert_compiles '"rescued"', %q{ def test diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index ba28af73b14fdb..8b3f4f84beb971 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -398,13 +398,13 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::IncrCounter(counter) => no_output!(gen_incr_counter(asm, counter)), Insn::ObjToString { val, cd, state, .. } => gen_objtostring(jit, asm, opnd!(val), *cd, &function.frame_state(*state)), &Insn::CheckInterrupts { state } => no_output!(gen_check_interrupts(jit, asm, &function.frame_state(state))), + &Insn::HashDup { val, state } => { gen_hash_dup(asm, opnd!(val), &function.frame_state(state)) }, &Insn::ArrayExtend { state, .. } | &Insn::ArrayMax { state, .. } | &Insn::ArrayPush { state, .. } | &Insn::DefinedIvar { state, .. } | &Insn::FixnumDiv { state, .. } | &Insn::FixnumMod { state, .. } - | &Insn::HashDup { state, .. } | &Insn::Send { state, .. } | &Insn::Throw { state, .. } | &Insn::ToArray { state, .. } @@ -684,6 +684,11 @@ fn gen_check_interrupts(jit: &mut JITState, asm: &mut Assembler, state: &FrameSt asm.jnz(side_exit(jit, state, SideExitReason::Interrupt)); } +fn gen_hash_dup(asm: &mut Assembler, val: Opnd, state: &FrameState) -> lir::Opnd { + gen_prepare_call_with_gc(asm, state); + asm_ccall!(asm, rb_hash_resurrect, val) +} + /// Compile an interpreter entry block to be inserted into an ISEQ fn gen_entry_prologue(asm: &mut Assembler, iseq: IseqPtr) { asm_comment!(asm, "ZJIT entry point: {}", iseq_get_location(iseq, 0)); From ec55b5b9aacf604634b6ca5b34366fba6e8b3f5e Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 28 Aug 2025 09:48:15 -0400 Subject: [PATCH 14/22] ZJIT: Generate code for ArrayPush --- test/ruby/test_zjit.rb | 9 +++++++++ zjit/src/codegen.rs | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 7c29fdeb193256..075a7765c880fc 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -109,6 +109,15 @@ def test }, insns: [:duphash] end + def test_pushtoarray + assert_compiles '[1, 2, 3]', %q{ + def test + [*[], 1, 2, 3] + end + test + }, insns: [:pushtoarray] + end + def test_setglobal_with_trace_var_exception assert_compiles '"rescued"', %q{ def test diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 8b3f4f84beb971..f257e5c13818d7 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -399,9 +399,9 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::ObjToString { val, cd, state, .. } => gen_objtostring(jit, asm, opnd!(val), *cd, &function.frame_state(*state)), &Insn::CheckInterrupts { state } => no_output!(gen_check_interrupts(jit, asm, &function.frame_state(state))), &Insn::HashDup { val, state } => { gen_hash_dup(asm, opnd!(val), &function.frame_state(state)) }, + &Insn::ArrayPush { array, val, state } => { no_output!(gen_array_push(asm, opnd!(array), opnd!(val), &function.frame_state(state))) }, &Insn::ArrayExtend { state, .. } | &Insn::ArrayMax { state, .. } - | &Insn::ArrayPush { state, .. } | &Insn::DefinedIvar { state, .. } | &Insn::FixnumDiv { state, .. } | &Insn::FixnumMod { state, .. } @@ -689,6 +689,11 @@ fn gen_hash_dup(asm: &mut Assembler, val: Opnd, state: &FrameState) -> lir::Opnd asm_ccall!(asm, rb_hash_resurrect, val) } +fn gen_array_push(asm: &mut Assembler, array: Opnd, val: Opnd, state: &FrameState) { + gen_prepare_call_with_gc(asm, state); + asm_ccall!(asm, rb_ary_push, array, val); +} + /// Compile an interpreter entry block to be inserted into an ISEQ fn gen_entry_prologue(asm: &mut Assembler, iseq: IseqPtr) { asm_comment!(asm, "ZJIT entry point: {}", iseq_get_location(iseq, 0)); From b108f11708228a69236b0eae4840992531af7025 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 28 Aug 2025 10:03:29 -0400 Subject: [PATCH 15/22] ZJIT: Generate code for ToArray, ToNewArray --- test/ruby/test_zjit.rb | 21 +++++++++++++++++++++ zjit/src/codegen.rs | 14 ++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 075a7765c880fc..52f2cddeb74efc 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -118,6 +118,27 @@ def test }, insns: [:pushtoarray] end + def test_splatarray_new_array + assert_compiles '[1, 2, 3]', %q{ + def test a + [*a, 3] + end + test [1, 2] + }, insns: [:splatarray] + end + + def test_splatarray_existing_array + assert_compiles '[1, 2, 3]', %q{ + def foo v + [1, 2, v] + end + def test a + foo(*a) + end + test [3] + }, insns: [:splatarray] + end + def test_setglobal_with_trace_var_exception assert_compiles '"rescued"', %q{ def test diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index f257e5c13818d7..d87c94cf8c2d72 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -400,6 +400,8 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::CheckInterrupts { state } => no_output!(gen_check_interrupts(jit, asm, &function.frame_state(state))), &Insn::HashDup { val, state } => { gen_hash_dup(asm, opnd!(val), &function.frame_state(state)) }, &Insn::ArrayPush { array, val, state } => { no_output!(gen_array_push(asm, opnd!(array), opnd!(val), &function.frame_state(state))) }, + &Insn::ToNewArray { val, state } => { gen_to_new_array(jit, asm, opnd!(val), &function.frame_state(state)) }, + &Insn::ToArray { val, state } => { gen_to_array(jit, asm, opnd!(val), &function.frame_state(state)) }, &Insn::ArrayExtend { state, .. } | &Insn::ArrayMax { state, .. } | &Insn::DefinedIvar { state, .. } @@ -407,8 +409,6 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio | &Insn::FixnumMod { state, .. } | &Insn::Send { state, .. } | &Insn::Throw { state, .. } - | &Insn::ToArray { state, .. } - | &Insn::ToNewArray { state, .. } => return Err(state), }; @@ -694,6 +694,16 @@ fn gen_array_push(asm: &mut Assembler, array: Opnd, val: Opnd, state: &FrameStat asm_ccall!(asm, rb_ary_push, array, val); } +fn gen_to_new_array(jit: &mut JITState, asm: &mut Assembler, val: Opnd, state: &FrameState) -> lir::Opnd { + gen_prepare_non_leaf_call(jit, asm, state); + asm_ccall!(asm, rb_vm_splat_array, Opnd::Value(Qtrue), val) +} + +fn gen_to_array(jit: &mut JITState, asm: &mut Assembler, val: Opnd, state: &FrameState) -> lir::Opnd { + gen_prepare_non_leaf_call(jit, asm, state); + asm_ccall!(asm, rb_vm_splat_array, Opnd::Value(Qfalse), val) +} + /// Compile an interpreter entry block to be inserted into an ISEQ fn gen_entry_prologue(asm: &mut Assembler, iseq: IseqPtr) { asm_comment!(asm, "ZJIT entry point: {}", iseq_get_location(iseq, 0)); From 07e28ba486e26172d76211080a82dc9cdb38250d Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 28 Aug 2025 10:27:02 -0400 Subject: [PATCH 16/22] ZJIT: Generate code for DefinedIvar --- test/ruby/test_zjit.rb | 14 ++++++++++++++ zjit.c | 7 +++++++ zjit/bindgen/src/main.rs | 1 + zjit/src/codegen.rs | 6 +++++- zjit/src/cruby_bindings.inc.rs | 1 + 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 52f2cddeb74efc..736c6306d13852 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -139,6 +139,20 @@ def test a }, insns: [:splatarray] end + def test_definedivar + assert_compiles '[nil, "instance-variable", nil]', %q{ + def test + v0 = defined?(@a) + @a = nil + v1 = defined?(@a) + remove_instance_variable :@a + v2 = defined?(@a) + [v0, v1, v2] + end + test + }, insns: [:definedivar] + end + def test_setglobal_with_trace_var_exception assert_compiles '"rescued"', %q{ def test diff --git a/zjit.c b/zjit.c index 9cc8b514232c03..d4798ef8605eff 100644 --- a/zjit.c +++ b/zjit.c @@ -356,6 +356,13 @@ rb_zjit_singleton_class_p(VALUE klass) return RCLASS_SINGLETON_P(klass); } +VALUE +rb_zjit_defined_ivar(VALUE obj, ID id, VALUE pushval) +{ + VALUE result = rb_ivar_defined(obj, id); + return result ? pushval : Qnil; +} + // Primitives used by zjit.rb. Don't put other functions below, which wouldn't use them. VALUE rb_zjit_assert_compiles(rb_execution_context_t *ec, VALUE self); VALUE rb_zjit_stats(rb_execution_context_t *ec, VALUE self, VALUE target_key); diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index a95b8dcaaa8594..e4c073d48a60e2 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -359,6 +359,7 @@ fn main() { .allowlist_function("rb_zjit_icache_invalidate") .allowlist_function("rb_zjit_print_exception") .allowlist_function("rb_zjit_singleton_class_p") + .allowlist_function("rb_zjit_defined_ivar") .allowlist_type("robject_offsets") .allowlist_type("rstring_offsets") .allowlist_var("RB_INVALID_SHAPE_ID") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index d87c94cf8c2d72..cdc62043fdaf3a 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -402,9 +402,9 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::ArrayPush { array, val, state } => { no_output!(gen_array_push(asm, opnd!(array), opnd!(val), &function.frame_state(state))) }, &Insn::ToNewArray { val, state } => { gen_to_new_array(jit, asm, opnd!(val), &function.frame_state(state)) }, &Insn::ToArray { val, state } => { gen_to_array(jit, asm, opnd!(val), &function.frame_state(state)) }, + &Insn::DefinedIvar { self_val, id, pushval, .. } => { gen_defined_ivar(asm, opnd!(self_val), id, pushval) }, &Insn::ArrayExtend { state, .. } | &Insn::ArrayMax { state, .. } - | &Insn::DefinedIvar { state, .. } | &Insn::FixnumDiv { state, .. } | &Insn::FixnumMod { state, .. } | &Insn::Send { state, .. } @@ -704,6 +704,10 @@ fn gen_to_array(jit: &mut JITState, asm: &mut Assembler, val: Opnd, state: &Fram asm_ccall!(asm, rb_vm_splat_array, Opnd::Value(Qfalse), val) } +fn gen_defined_ivar(asm: &mut Assembler, self_val: Opnd, id: ID, pushval: VALUE) -> lir::Opnd { + asm_ccall!(asm, rb_zjit_defined_ivar, self_val, id.0.into(), Opnd::Value(pushval)) +} + /// Compile an interpreter entry block to be inserted into an ISEQ fn gen_entry_prologue(asm: &mut Assembler, iseq: IseqPtr) { asm_comment!(asm, "ZJIT entry point: {}", iseq_get_location(iseq, 0)); diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index d10e3cc8a0eab7..33f7e0b3e62727 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -944,6 +944,7 @@ unsafe extern "C" { pub fn rb_zjit_print_exception(); pub fn rb_zjit_shape_obj_too_complex_p(obj: VALUE) -> bool; pub fn rb_zjit_singleton_class_p(klass: VALUE) -> bool; + pub fn rb_zjit_defined_ivar(obj: VALUE, id: ID, pushval: VALUE) -> VALUE; pub fn rb_iseq_encoded_size(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; pub fn rb_iseq_pc_at_idx(iseq: *const rb_iseq_t, insn_idx: u32) -> *mut VALUE; pub fn rb_iseq_opcode_at_pc(iseq: *const rb_iseq_t, pc: *const VALUE) -> ::std::os::raw::c_int; From 85217252250a796ceca5c6a4be2e07bfedaaba60 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 28 Aug 2025 10:36:13 -0400 Subject: [PATCH 17/22] ZJIT: Generate code for ArrayExtend --- test/ruby/test_zjit.rb | 9 +++++++++ zjit/bindgen/src/main.rs | 1 + zjit/src/codegen.rs | 9 +++++++-- zjit/src/cruby_bindings.inc.rs | 1 + 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 736c6306d13852..e2a0120eb07ee3 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -139,6 +139,15 @@ def test a }, insns: [:splatarray] end + def test_concattoarray + assert_compiles '[1, 2, 3]', %q{ + def test(*a) + [1, 2, *a] + end + test 3 + }, insns: [:concattoarray] + end + def test_definedivar assert_compiles '[nil, "instance-variable", nil]', %q{ def test diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index e4c073d48a60e2..0eb800e1f99f72 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -145,6 +145,7 @@ fn main() { .allowlist_function("rb_ary_store") .allowlist_function("rb_ary_resurrect") .allowlist_function("rb_ary_cat") + .allowlist_function("rb_ary_concat") .allowlist_function("rb_ary_clear") .allowlist_function("rb_ary_dup") .allowlist_function("rb_ary_push") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index cdc62043fdaf3a..189741b07ce5e7 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -403,8 +403,8 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::ToNewArray { val, state } => { gen_to_new_array(jit, asm, opnd!(val), &function.frame_state(state)) }, &Insn::ToArray { val, state } => { gen_to_array(jit, asm, opnd!(val), &function.frame_state(state)) }, &Insn::DefinedIvar { self_val, id, pushval, .. } => { gen_defined_ivar(asm, opnd!(self_val), id, pushval) }, - &Insn::ArrayExtend { state, .. } - | &Insn::ArrayMax { state, .. } + &Insn::ArrayExtend { left, right, state } => { no_output!(gen_array_extend(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state))) }, + &Insn::ArrayMax { state, .. } | &Insn::FixnumDiv { state, .. } | &Insn::FixnumMod { state, .. } | &Insn::Send { state, .. } @@ -708,6 +708,11 @@ fn gen_defined_ivar(asm: &mut Assembler, self_val: Opnd, id: ID, pushval: VALUE) asm_ccall!(asm, rb_zjit_defined_ivar, self_val, id.0.into(), Opnd::Value(pushval)) } +fn gen_array_extend(jit: &mut JITState, asm: &mut Assembler, left: Opnd, right: Opnd, state: &FrameState) { + gen_prepare_non_leaf_call(jit, asm, state); + asm_ccall!(asm, rb_ary_concat, left, right); +} + /// Compile an interpreter entry block to be inserted into an ISEQ fn gen_entry_prologue(asm: &mut Assembler, iseq: IseqPtr) { asm_comment!(asm, "ZJIT entry point: {}", iseq_get_location(iseq, 0)); diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 33f7e0b3e62727..4975308f68d6b6 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -783,6 +783,7 @@ unsafe extern "C" { pub fn rb_ary_cat(ary: VALUE, train: *const VALUE, len: ::std::os::raw::c_long) -> VALUE; pub fn rb_ary_push(ary: VALUE, elem: VALUE) -> VALUE; pub fn rb_ary_clear(ary: VALUE) -> VALUE; + pub fn rb_ary_concat(lhs: VALUE, rhs: VALUE) -> VALUE; pub fn rb_hash_new() -> VALUE; pub fn rb_hash_aref(hash: VALUE, key: VALUE) -> VALUE; pub fn rb_hash_aset(hash: VALUE, key: VALUE, val: VALUE) -> VALUE; From 7d670ead3008284822dea0e4c62ec7f2d12a2aec Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 28 Aug 2025 10:20:15 -0700 Subject: [PATCH 18/22] ZJIT: Disable profiling in compile_iseq (#14385) This catches both the interpreter-JIT and JIT-JIT cases. Fixes https://github.com/Shopify/ruby/issues/719 --- zjit.c | 3 --- zjit/src/codegen.rs | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/zjit.c b/zjit.c index d4798ef8605eff..311bc1fe8098e6 100644 --- a/zjit.c +++ b/zjit.c @@ -165,9 +165,6 @@ rb_zjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit RB_VM_LOCKING() { rb_vm_barrier(); - // Convert ZJIT instructions back to bare instructions - rb_zjit_profile_disable(iseq); - // Compile a block version starting at the current instruction uint8_t *rb_zjit_iseq_gen_entry_point(const rb_iseq_t *iseq, rb_execution_context_t *ec); // defined in Rust uintptr_t code_ptr = (uintptr_t)rb_zjit_iseq_gen_entry_point(iseq, ec); diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 189741b07ce5e7..023498e3be68f0 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1398,6 +1398,9 @@ pub fn local_size_and_idx_to_bp_offset(local_size: usize, local_idx: usize) -> i /// Convert ISEQ into High-level IR fn compile_iseq(iseq: IseqPtr) -> Option { + // Convert ZJIT instructions back to bare instructions + unsafe { crate::cruby::rb_zjit_profile_disable(iseq) }; + // Reject ISEQs with very large temp stacks. // We cannot encode too large offsets to access locals in arm64. let stack_max = unsafe { rb_get_iseq_body_stack_max(iseq) }; From c2d99d06474c22bdf03bba8f46c07a185046f970 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 28 Aug 2025 10:31:46 -0700 Subject: [PATCH 19/22] ZJIT: Fix a typo follow-up on https://github.com/ruby/ruby/pull/14378 --- zjit/src/stats.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 5d0a66ff2c263f..34fe38b10dfc57 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -224,7 +224,7 @@ pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE, target_key: VALUE) -> let total_insn_count = vm_insn_count + counters.zjit_insn_count; set_stat_usize!(hash, "total_insn_count", total_insn_count); - set_stat_f64!(hash, "ratio_in_yjit", 100.0 * vm_insn_count as f64 / total_insn_count as f64); + set_stat_f64!(hash, "ratio_in_zjit", 100.0 * vm_insn_count as f64 / total_insn_count as f64); } hash From b47ea34a9b9e602e70bb69478c4abd2d9f1651d0 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 28 Aug 2025 10:34:22 -0700 Subject: [PATCH 20/22] ZJIT: Fix a flipped stat I'm sorry. Another follow-up on https://github.com/ruby/ruby/pull/14378 --- zjit/src/stats.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 34fe38b10dfc57..2be581fa2bfdf6 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -224,7 +224,7 @@ pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE, target_key: VALUE) -> let total_insn_count = vm_insn_count + counters.zjit_insn_count; set_stat_usize!(hash, "total_insn_count", total_insn_count); - set_stat_f64!(hash, "ratio_in_zjit", 100.0 * vm_insn_count as f64 / total_insn_count as f64); + set_stat_f64!(hash, "ratio_in_zjit", 100.0 * counters.zjit_insn_count as f64 / total_insn_count as f64); } hash From fa3c23eb811c7bee7c9ed945c8f12cb40ecd9b6a Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 28 Aug 2025 18:50:22 +0100 Subject: [PATCH 21/22] ZJIT: Prepare getglobal for non-leaf call (#14387) Depending on the user's warning level, getting certain global variables may lead to calling `Warning#warn`, which can be redefined by the user. This fixes another `bootstraptest/test_yjit.rb` failure. --- test/ruby/test_zjit.rb | 21 +++++++++++++++++++++ zjit/src/codegen.rs | 7 +++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index e2a0120eb07ee3..3ff1392ed71887 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -78,6 +78,27 @@ def test(n) = n } end + def test_getglobal_with_warning + assert_compiles('"rescued"', %q{ + Warning[:deprecated] = true + + module Warning + def warn(message) + raise + end + end + + def test + $= + rescue + "rescued" + end + + $VERBOSE = true + test + }, insns: [:getglobal]) + end + def test_setglobal assert_compiles '1', %q{ def test diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 023498e3be68f0..6c55b3fb5db5eb 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -384,7 +384,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::CCall { cfun, args, name: _, return_type: _, elidable: _ } => gen_ccall(asm, *cfun, opnds!(args)), Insn::GetIvar { self_val, id, state: _ } => gen_getivar(asm, opnd!(self_val), *id), Insn::SetGlobal { id, val, state } => no_output!(gen_setglobal(jit, asm, *id, opnd!(val), &function.frame_state(*state))), - Insn::GetGlobal { id, state: _ } => gen_getglobal(asm, *id), + Insn::GetGlobal { id, state } => gen_getglobal(jit, asm, *id, &function.frame_state(*state)), &Insn::GetLocal { ep_offset, level } => gen_getlocal_with_ep(asm, ep_offset, level), &Insn::SetLocal { val, ep_offset, level } => no_output!(gen_setlocal_with_ep(asm, opnd!(val), function.type_of(val), ep_offset, level)), Insn::GetConstantPath { ic, state } => gen_get_constant_path(jit, asm, *ic, &function.frame_state(*state)), @@ -609,7 +609,10 @@ fn gen_setivar(asm: &mut Assembler, recv: Opnd, id: ID, val: Opnd) { } /// Look up global variables -fn gen_getglobal(asm: &mut Assembler, id: ID) -> Opnd { +fn gen_getglobal(jit: &mut JITState, asm: &mut Assembler, id: ID, state: &FrameState) -> Opnd { + // `Warning` module's method `warn` can be called when reading certain global variables + gen_prepare_non_leaf_call(jit, asm, state); + asm_ccall!(asm, rb_gvar_get, id.0.into()) } From 041450ad417caa5b12a32b38ae5ec54618f0c792 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 28 Aug 2025 16:45:45 +0200 Subject: [PATCH 22/22] rb_ivar_delete: also re-embed T_IMEMO/fields Right now JITs don't generate any code to access ivar on types other than T_OBJECT, but they might soon, so we must ensure two IMEMO/fields can't have the same `shape_id` but diffent embed/heap status. --- variable.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/variable.c b/variable.c index 690b7a26cf4997..f80f1a56a6051a 100644 --- a/variable.c +++ b/variable.c @@ -1614,12 +1614,12 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) MEMMOVE(&fields[removed_index], &fields[removed_index + 1], VALUE, trailing_fields); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); - if (type == T_OBJECT && FL_TEST_RAW(obj, ROBJECT_HEAP) && rb_obj_embedded_size(new_fields_count) <= rb_gc_obj_slot_size(obj)) { + if (FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP) && rb_obj_embedded_size(new_fields_count) <= rb_gc_obj_slot_size(fields_obj)) { // Re-embed objects when instances become small enough // This is necessary because YJIT assumes that objects with the same shape // have the same embeddedness for efficiency (avoid extra checks) - FL_UNSET_RAW(obj, ROBJECT_HEAP); - MEMCPY(ROBJECT_FIELDS(obj), fields, VALUE, new_fields_count); + FL_UNSET_RAW(fields_obj, ROBJECT_HEAP); + MEMCPY(rb_imemo_fields_ptr(fields_obj), fields, VALUE, new_fields_count); xfree(fields); } }