From ce75c10810b54e45c8987ec5d49109c22b82921c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 22 Sep 2026 09:36:30 -0400 Subject: [PATCH 01/31] Allow disabling `zjit-stats` in tool/zjit_diff.rb (#18893) When running benchmarks, sometimes there is a overhead in the statistics collection, so it can be useful to disable `zjit-stats`. In my case I was investigating https://github.com/Shopify/ruby/issues/1051 and was confused why I was not seeing any new instruction being generated in the LIR but there was still performance difference when running zjit-diff. The cause for the performance difference in that PR was new instructions to increment stats counters. So this PR allow running the tool without stats enabled. I'm even wondering if the default should not be with it disabled. --- tool/zjit_diff.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tool/zjit_diff.rb b/tool/zjit_diff.rb index 4f8f74d20f6225..50fd8cf8f4b28c 100755 --- a/tool/zjit_diff.rb +++ b/tool/zjit_diff.rb @@ -53,10 +53,11 @@ def bench! def run_benchmarks(ruby_bench_path) Dir.chdir(ruby_bench_path) do + zjit_stats = @options[:zjit_stats] ? ' --zjit-stats' : '' @runner.cmd({ 'RUBIES_DIR' => RUBIES_DIR }, './run_benchmarks.rb', '--chruby', - "before::#{@before_hash} --zjit-stats;after::#{@after_hash} --zjit-stats", + "before::#{@before_hash}#{zjit_stats};after::#{@after_hash}#{zjit_stats}", '--out-name', DATA_FILENAME, *@options[:bench_args], @@ -177,7 +178,7 @@ def parse_ref(ref) DEFAULT_BENCHMARKS = %w[lobsters railsbench].freeze -options = {} +options = { zjit_stats: true } subtext = <<~HELP Subcommands: @@ -231,6 +232,10 @@ def parse_ref(ref) options[:force_rebuild] = true end + opts.on('--[no-]zjit-stats', 'Pass --zjit-stats to each Ruby command') do |zjit_stats| + options[:zjit_stats] = zjit_stats + end + opts.on('--quiet', 'Silence output of commands except for benchmark result') do options[:quiet] = true end From 1070504b89be433a1af1498e2fad51661ae1b315 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 22 Sep 2026 09:39:58 -0400 Subject: [PATCH 02/31] Make non-Array Struct __members__ a bug (#18888) We should not ever hit this from Ruby code; user code cannot modify ivars that do not start with @. This makes `num_members` leaf and therefore Struct class allocation also leaf. --- struct.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/struct.c b/struct.c index 8bc1694700bdc5..c1efe715f7c896 100644 --- a/struct.c +++ b/struct.c @@ -711,7 +711,7 @@ num_members(VALUE klass) VALUE members; members = struct_ivar_get(klass, id_members); if (!RB_TYPE_P(members, T_ARRAY)) { - rb_raise(rb_eTypeError, "broken members"); + rb_bug("broken members"); /* should never happen */ } return RARRAY_LEN(members); } From 7ba699ae26fd11c9021f16f9634e3dc3d01b3f7d Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Tue, 25 Aug 2026 13:19:37 -0400 Subject: [PATCH 03/31] Make fibers RUBY_TYPED_THREAD_SAFE_FREE Now fibers can be collected by multiple ractors at once when those Ractors are running local GCs. Fibers are created in the Ractor that owns them, so if multiple Ractors have unmarked fibers during local GC sweeping, we can now collect them concurrently instead of deferring their collection to a postponed job. Each fiber pool is protected by its own native mutex. In the future, maybe fiber pools will be ractor-local, but we have to experiment with it first and see if it's enough of a performance win to justify the change. We also want to minimize resource acquisition for each Ractor as they're supposed to be lighweight. --- cont.c | 263 +++++++++++++++++++++++++++++++++++++----------- internal/cont.h | 2 + thread.c | 1 + vm_core.h | 7 +- 4 files changed, 214 insertions(+), 59 deletions(-) diff --git a/cont.c b/cont.c index c4d57d92ba1a0d..9c29d509290f1c 100644 --- a/cont.c +++ b/cont.c @@ -175,6 +175,12 @@ struct fiber_pool_allocation { struct fiber_pool_allocation * next; }; +#if VM_CHECK_MODE > 0 && defined(HAVE_PTHREAD_H) +#define ASSERT_FIBER_POOL_LOCK_OWNER_P (true) +#else +#define ASSERT_FIBER_POOL_LOCK_OWNER_P (false) +#endif + // A fiber pool manages vacant stacks to reduce the overhead of creating fibers. struct fiber_pool { // A singly-linked list of allocations which contain 1 or more stacks each. @@ -206,6 +212,16 @@ struct fiber_pool { // The amount to allocate for the vm_stack. size_t vm_stack_size; + + // Pools are independent so each one has its own lock. + rb_nativethread_lock_t lock; + +#if ASSERT_FIBER_POOL_LOCK_OWNER_P + pthread_t lock_owner; +#endif + + // Links all live pools together so fork can reinitialize their locks. + struct ccan_list_node list_node; }; // Continuation contexts used by JITs @@ -287,13 +303,118 @@ rb_free_shared_fiber_pool(void) struct fiber_pool_allocation *allocations = shared_fiber_pool.allocations; while (allocations) { struct fiber_pool_allocation *next = allocations->next; - SIZED_FREE(allocations); + ruby_mimfree(allocations); allocations = next; } } static ID fiber_initialize_keywords[3] = {0}; +static CCAN_LIST_HEAD(fiber_pool_list); +static rb_nativethread_lock_t fiber_pool_list_lock; + +#if ASSERT_FIBER_POOL_LOCK_OWNER_P +static inline bool +fiber_pool_locked_p(const struct fiber_pool * fiber_pool) +{ + return pthread_equal(pthread_self(), fiber_pool->lock_owner); +} + +static inline void +ASSERT_fiber_pool_locked(const struct fiber_pool * fiber_pool) +{ +#if VM_CHECK_MODE == 0 + if (!rb_multi_ractor_p()) { + return; + } +#endif + VM_ASSERT(fiber_pool_locked_p(fiber_pool)); +} + +static inline void +ASSERT_fiber_pool_unlocked(const struct fiber_pool * fiber_pool) +{ +#if VM_CHECK_MODE == 0 + if (!rb_multi_ractor_p()) { + return; + } +#endif + VM_ASSERT(!fiber_pool_locked_p(fiber_pool)); +} +#else +#define ASSERT_fiber_pool_locked(fiber_pool) (void)0 +#define ASSERT_fiber_pool_unlocked(fiber_pool) (void)0 +#endif + +static inline void +fiber_pool_lock(struct fiber_pool * fiber_pool) +{ +#if VM_CHECK_MODE == 0 + // Locking isn't necessary when there's only 1 Ractor + if (!rb_multi_ractor_p()) return; +#endif + ASSERT_fiber_pool_unlocked(fiber_pool); + rb_native_mutex_lock(&fiber_pool->lock); +#if ASSERT_FIBER_POOL_LOCK_OWNER_P + fiber_pool->lock_owner = pthread_self(); +#endif +} + +static inline void +fiber_pool_unlock(struct fiber_pool * fiber_pool) +{ +#if VM_CHECK_MODE == 0 + if (!rb_multi_ractor_p()) return; +#endif + ASSERT_fiber_pool_locked(fiber_pool); +#if ASSERT_FIBER_POOL_LOCK_OWNER_P + fiber_pool->lock_owner = 0; +#endif + rb_native_mutex_unlock(&fiber_pool->lock); +} + +static void +fiber_pool_lock_initialize(struct fiber_pool * fiber_pool) +{ + rb_native_mutex_initialize(&fiber_pool->lock); +#if ASSERT_FIBER_POOL_LOCK_OWNER_P + fiber_pool->lock_owner = 0; +#endif +} + +static void +fiber_pool_list_add(struct fiber_pool * fiber_pool) +{ + rb_native_mutex_lock(&fiber_pool_list_lock); + { + ccan_list_add(&fiber_pool_list, &fiber_pool->list_node); + } + rb_native_mutex_unlock(&fiber_pool_list_lock); +} + +#ifdef RB_EXPERIMENTAL_FIBER_POOL +static void +fiber_pool_list_remove(struct fiber_pool * fiber_pool) +{ + rb_native_mutex_lock(&fiber_pool_list_lock); + { + ccan_list_del(&fiber_pool->list_node); + } + rb_native_mutex_unlock(&fiber_pool_list_lock); +} +#endif + +void +rb_fiber_pool_lock_atfork(void) +{ + rb_native_mutex_initialize(&fiber_pool_list_lock); + + struct fiber_pool *fiber_pool = NULL; + ccan_list_for_each(&fiber_pool_list, fiber_pool, list_node) { + fiber_pool_lock_initialize(fiber_pool); + } +} + /* * FreeBSD require a first (i.e. addr) argument of mmap(2) is not NULL * if MAP_STACK is passed. @@ -397,6 +518,7 @@ fiber_pool_vacancy_reset(struct fiber_pool_vacancy * vacancy) inline static struct fiber_pool_vacancy * fiber_pool_vacancy_push(struct fiber_pool_vacancy * vacancy, struct fiber_pool_vacancy * head) { + ASSERT_fiber_pool_locked(vacancy->stack.pool); vacancy->next = head; #ifdef FIBER_POOL_ALLOCATION_FREE @@ -429,6 +551,7 @@ fiber_pool_vacancy_remove(struct fiber_pool_vacancy * vacancy) inline static struct fiber_pool_vacancy * fiber_pool_vacancy_pop(struct fiber_pool * pool) { + ASSERT_fiber_pool_locked(pool); struct fiber_pool_vacancy * vacancy = pool->vacancies; if (vacancy) { @@ -441,6 +564,7 @@ fiber_pool_vacancy_pop(struct fiber_pool * pool) inline static struct fiber_pool_vacancy * fiber_pool_vacancy_pop(struct fiber_pool * pool) { + ASSERT_fiber_pool_locked(pool); struct fiber_pool_vacancy * vacancy = pool->vacancies; if (vacancy) { @@ -516,20 +640,26 @@ fiber_pool_allocate_memory(size_t * count, size_t stride) // Given an existing fiber pool, expand it by the specified number of stacks. // // @param count the maximum number of stacks to allocate. +// @param needs_lock whether this function should acquire the fiber pool lock +// @param vacancy_out the out param for the next vacancy, if set to non-NULL // @return the new allocation on success, or NULL on failure with errno set. -// @raise NoMemoryError if the struct or memory allocation fails. // -// Call from fiber_pool_stack_acquire_expand with VM lock held, or from -// fiber_pool_initialize before the pool is shared across threads. // @sa fiber_pool_allocation_free static struct fiber_pool_allocation * -fiber_pool_expand(struct fiber_pool * fiber_pool, size_t count) +fiber_pool_expand(struct fiber_pool * fiber_pool, size_t count, bool needs_lock, struct fiber_pool_vacancy **vacancy_out) { if (count == 0) { errno = EAGAIN; return NULL; } + struct fiber_pool_allocation * allocation = ruby_mimmalloc(sizeof(struct fiber_pool_allocation)); + if (RB_UNLIKELY(!allocation)) { + errno = ENOMEM; + return NULL; + } + if (needs_lock) fiber_pool_lock(fiber_pool); + STACK_GROW_DIR_DETECTION; size_t size = fiber_pool->size; @@ -538,6 +668,8 @@ fiber_pool_expand(struct fiber_pool * fiber_pool, size_t count) // If the maximum number of stacks is set, and we have reached it, return NULL. if (fiber_pool->maximum_count > 0) { if (fiber_pool->count >= fiber_pool->maximum_count) { + if (needs_lock) fiber_pool_unlock(fiber_pool); + ruby_mimfree(allocation); errno = EAGAIN; return NULL; } @@ -547,16 +679,15 @@ fiber_pool_expand(struct fiber_pool * fiber_pool, size_t count) } } - // Allocate metadata before mmap: ruby_xmalloc (RB_ALLOC) raises on failure and - // must not run after base is mapped, or the region would leak. - struct fiber_pool_allocation * allocation = RB_ALLOC(struct fiber_pool_allocation); - // Allocate the memory required for the stacks: void * base = fiber_pool_allocate_memory(&count, stride); if (base == NULL) { - if (!errno) errno = ENOMEM; - ruby_xfree(allocation); + int saved_errno = errno; + if (!saved_errno) saved_errno = ENOMEM; + if (needs_lock) fiber_pool_unlock(fiber_pool); + ruby_mimfree(allocation); + errno = saved_errno; return NULL; } @@ -586,8 +717,9 @@ fiber_pool_expand(struct fiber_pool * fiber_pool, size_t count) if (!VirtualProtect(page, RB_PAGE_SIZE, PAGE_READWRITE | PAGE_GUARD, &old_protect)) { int error = rb_w32_map_errno(GetLastError()); + if (needs_lock) fiber_pool_unlock(fiber_pool); VirtualFree(allocation->base, 0, MEM_RELEASE); - ruby_xfree(allocation); + ruby_mimfree(allocation); errno = error; return NULL; } @@ -597,9 +729,10 @@ fiber_pool_expand(struct fiber_pool * fiber_pool, size_t count) #else if (mprotect(page, RB_PAGE_SIZE, PROT_NONE) < 0) { int error = errno; + if (needs_lock) fiber_pool_unlock(fiber_pool); if (!error) error = ENOMEM; munmap(allocation->base, count*stride); - ruby_xfree(allocation); + ruby_mimfree(allocation); errno = error; return NULL; } @@ -631,9 +764,29 @@ fiber_pool_expand(struct fiber_pool * fiber_pool, size_t count) fiber_pool->vacancies = vacancies; fiber_pool->count += count; + if (vacancy_out) { + *vacancy_out = fiber_pool_vacancy_pop(fiber_pool); + } + if (needs_lock) fiber_pool_unlock(fiber_pool); + return allocation; } +static struct fiber_pool_vacancy * +fiber_pool_expand_and_pop(struct fiber_pool * fiber_pool, size_t count) +{ + ASSERT_fiber_pool_locked(fiber_pool); + struct fiber_pool_vacancy *vacancy_out = NULL; + struct fiber_pool_allocation *allocation = fiber_pool_expand(fiber_pool, count, false, &vacancy_out); + if (allocation) { + VM_ASSERT(vacancy_out); + return vacancy_out; + } + else { + return NULL; + } +} + // Initialize the specified fiber pool with the given number of stacks. // @param vm_stack_size The size of the vm stack to allocate. static void @@ -651,8 +804,11 @@ fiber_pool_initialize(struct fiber_pool * fiber_pool, size_t size, size_t minimu fiber_pool->used = 0; fiber_pool->vm_stack_size = vm_stack_size; + fiber_pool_lock_initialize(fiber_pool); + fiber_pool_list_add(fiber_pool); + if (fiber_pool->minimum_count > 0) { - if (RB_UNLIKELY(!fiber_pool_expand(fiber_pool, fiber_pool->minimum_count))) { + if (RB_UNLIKELY(!fiber_pool_expand(fiber_pool, fiber_pool->minimum_count, true, NULL))) { rb_raise(rb_eFiberError, "can't allocate initial fiber stacks (%"PRIuSIZE" x %"PRIuSIZE" bytes): %s", fiber_pool->minimum_count, fiber_pool->size, strerror(errno)); } } @@ -699,7 +855,7 @@ fiber_pool_allocation_free(struct fiber_pool_allocation * allocation) allocation->pool->count -= allocation->count; - SIZED_FREE(allocation); + ruby_mimfree(allocation); } #endif @@ -707,6 +863,7 @@ fiber_pool_allocation_free(struct fiber_pool_allocation * allocation) static size_t fiber_pool_stack_expand_count(const struct fiber_pool *pool) { + ASSERT_fiber_pool_locked(pool); const size_t maximum_allocations = FIBER_POOL_MAXIMUM_ALLOCATIONS; const size_t minimum_count = FIBER_POOL_MINIMUM_COUNT; @@ -732,24 +889,29 @@ fiber_pool_stack_expand_count(const struct fiber_pool *pool) return count; } -// When the vacancy list is empty, grow the pool (and run GC only if mmap fails). Caller holds the VM lock. +// When the vacancy list is empty, grow the pool (and run GC only if mmap fails). // Returns NULL if expansion failed after GC + retry; errno is set. Otherwise returns a vacancy. static struct fiber_pool_vacancy * fiber_pool_stack_acquire_expand(struct fiber_pool *fiber_pool) { + ASSERT_fiber_pool_locked(fiber_pool); size_t count = fiber_pool_stack_expand_count(fiber_pool); if (DEBUG_ACQUIRE) fprintf(stderr, "fiber_pool_stack_acquire: expanding fiber pool by %"PRIuSIZE" stacks\n", count); struct fiber_pool_vacancy *vacancy = NULL; - if (RB_LIKELY(fiber_pool_expand(fiber_pool, count))) { - return fiber_pool_vacancy_pop(fiber_pool); + if (RB_LIKELY((vacancy = fiber_pool_expand_and_pop(fiber_pool, count)))) { + return vacancy; } else { if (DEBUG_ACQUIRE) fprintf(stderr, "fiber_pool_stack_acquire: expand failed (%s), collecting garbage\n", strerror(errno)); - rb_gc(); + fiber_pool_unlock(fiber_pool); + { + rb_gc(); + } + fiber_pool_lock(fiber_pool); // After running GC, the vacancy list may have some stacks: vacancy = fiber_pool_vacancy_pop(fiber_pool); @@ -761,11 +923,11 @@ fiber_pool_stack_acquire_expand(struct fiber_pool *fiber_pool) count = fiber_pool_stack_expand_count(fiber_pool); // Try to expand the fiber pool again: - if (RB_LIKELY(fiber_pool_expand(fiber_pool, count))) { - return fiber_pool_vacancy_pop(fiber_pool); + if (RB_LIKELY((vacancy = fiber_pool_expand_and_pop(fiber_pool, count)))) { + return vacancy; } else { - // Okay, we really failed to acquire a stack. Give up and return NULL with errno set: + // Okay, we really failed to acquire a stack. Give up and return NULL with errno set return NULL; } } @@ -777,8 +939,7 @@ fiber_pool_stack_acquire(struct fiber_pool * fiber_pool) { struct fiber_pool_vacancy * vacancy; - unsigned int lev; - RB_VM_LOCK_ENTER_LEV(&lev); + fiber_pool_lock(fiber_pool); { // Fast path: try to acquire a stack from the vacancy list: vacancy = fiber_pool_vacancy_pop(fiber_pool); @@ -791,7 +952,7 @@ fiber_pool_stack_acquire(struct fiber_pool * fiber_pool) // If expansion failed, raise an error: if (RB_UNLIKELY(!vacancy)) { - RB_VM_LOCK_LEAVE_LEV(&lev); + fiber_pool_unlock(fiber_pool); rb_raise(rb_eFiberError, "can't allocate fiber stack: %s", strerror(errno)); } } @@ -812,7 +973,7 @@ fiber_pool_stack_acquire(struct fiber_pool * fiber_pool) fiber_pool_stack_reset(&vacancy->stack); } - RB_VM_LOCK_LEAVE_LEV(&lev); + fiber_pool_unlock(fiber_pool); return vacancy->stack; } @@ -852,24 +1013,12 @@ fiber_pool_stack_free(struct fiber_pool_stack * stack) static void fiber_pool_stack_release(struct fiber_pool_stack * stack) { + ASSERT_fiber_pool_locked(stack->pool); struct fiber_pool * pool = stack->pool; struct fiber_pool_vacancy * vacancy = fiber_pool_vacancy_pointer(stack->base, stack->size); if (DEBUG) fprintf(stderr, "fiber_pool_stack_release: %p used=%"PRIuSIZE"\n", stack->base, stack->pool->used); - /* Serialize pool access against other Ractors' acquires: a per-Ractor GC sweep can - * free a fiber without the VM lock. Releases are rare, so take it NO_BARRIER, - * never joining a forming global barrier. - * - * Two callers must not take it. VM destruct's free-at-exit walk is single-threaded - * and its thread structs are already freed, so looking the current Ractor up would - * read freed memory. A single objspace impl (mmtk) frees on its own GC thread, - * which has no execution context to look one up from at all -- and it stops the - * world, so nothing races us there. */ - unsigned int lev = 0; - const bool lock_here = !ruby_vm_during_cleanup && rb_current_execution_context(false) != NULL; - if (lock_here) RB_VM_LOCK_ENTER_LEV_NB(&lev); - // Copy the stack details into the vacancy area: vacancy->stack = *stack; // After this point, be careful about updating/using state in stack, since it's copied to the vacancy area. @@ -900,8 +1049,6 @@ fiber_pool_stack_release(struct fiber_pool_stack * stack) fiber_pool_stack_free(&vacancy->stack); } #endif - - if (lock_here) RB_VM_LOCK_LEAVE_LEV_NB(&lev); } static inline void @@ -1006,7 +1153,14 @@ fiber_stack_release(rb_fiber_t * fiber) // Return the stack back to the fiber pool if it wasn't already: if (fiber->stack.base) { - fiber_pool_stack_release(&fiber->stack); + struct fiber_pool * fiber_pool = fiber->stack.pool; + + fiber_pool_lock(fiber_pool); + { + fiber_pool_stack_release(&fiber->stack); + } + fiber_pool_unlock(fiber_pool); + fiber->stack.base = NULL; } @@ -1014,15 +1168,6 @@ fiber_stack_release(rb_fiber_t * fiber) rb_ec_clear_vm_stack(ec); } -static void -fiber_stack_release_locked(rb_fiber_t *fiber) -{ - /* Called from GC finalization. With per-Ractor objspaces the sweep runs with - * no barrier and no VM lock, so the side that returns stacks to the pool - * (fiber_pool_stack_release) takes the lock. Do not assert the VM lock here. */ - fiber_stack_release(fiber); -} - static const char * fiber_status_name(enum fiber_status s) { @@ -1185,7 +1330,7 @@ cont_free(void *ptr) else { rb_fiber_t *fiber = (rb_fiber_t*)cont; coroutine_destroy(&fiber->context); - fiber_stack_release_locked(fiber); + fiber_stack_release(fiber); } SIZED_FREE_N(cont->saved_vm_stack.ptr, cont->saved_vm_stack.size); @@ -1369,7 +1514,7 @@ cont_handle_weak_references(void *ptr) static const rb_data_type_t rb_cont_data_type = { "continuation", {cont_mark, cont_free, cont_memsize, cont_compact, cont_handle_weak_references}, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY + 0, 0, RUBY_TYPED_THREAD_SAFE_FREE }; static inline void @@ -2130,7 +2275,7 @@ fiber_handle_weak_references(void *ptr) static const rb_data_type_t rb_fiber_data_type = { "fiber", {fiber_mark, fiber_free, fiber_memsize, fiber_compact, fiber_handle_weak_references}, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY + 0, 0, RUBY_TYPED_THREAD_SAFE_FREE }; static VALUE fiber_alloc_in(VALUE klass, void *objspace); @@ -2908,9 +3053,7 @@ fiber_switch(rb_fiber_t *fiber, int argc, const VALUE *argv, int kw_splat, rb_fi // We cannot free the stack until the pthread is joined: #ifndef COROUTINE_PTHREAD_CONTEXT if (FIBER_TERMINATED_P(fiber)) { - RB_VM_LOCKING() { - fiber_stack_release(fiber); - } + fiber_stack_release(fiber); } #endif RB_GC_GUARD(fiber_value); @@ -3537,7 +3680,9 @@ fiber_pool_free(void *ptr) struct fiber_pool * fiber_pool = ptr; RUBY_FREE_ENTER("fiber_pool"); + fiber_pool_list_remove(fiber_pool); fiber_pool_allocation_free(fiber_pool->allocations); + rb_native_mutex_destroy(&fiber_pool->lock); SIZED_FREE(fiber_pool); RUBY_FREE_LEAVE("fiber_pool"); @@ -3557,7 +3702,7 @@ fiber_pool_memsize(const void *ptr) static const rb_data_type_t FiberPoolDataType = { "fiber_pool", {NULL, fiber_pool_free, fiber_pool_memsize,}, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY + 0, 0, RUBY_TYPED_THREAD_SAFE_FREE }; static VALUE @@ -3670,6 +3815,8 @@ Init_Cont(void) rb_eFiberError = rb_define_class("FiberError", rb_eStandardError); + rb_native_mutex_initialize(&fiber_pool_list_lock); + size_t minimum_count = shared_fiber_pool_minimum_count(); size_t maximum_count = shared_fiber_pool_maximum_count(); fiber_pool_initialize(&shared_fiber_pool, stack_size, minimum_count, maximum_count, vm_stack_size); diff --git a/internal/cont.h b/internal/cont.h index f9f200e420379b..9878db639ac46f 100644 --- a/internal/cont.h +++ b/internal/cont.h @@ -20,6 +20,8 @@ void ruby_register_rollback_func_for_ensure(VALUE (*ensure_func)(VALUE), VALUE ( /* vm.c */ void rb_free_shared_fiber_pool(void); +/* thread.c */ +void rb_fiber_pool_lock_atfork(void); // Copy locals from the current execution to the specified fiber. VALUE rb_fiber_inherit_storage(struct rb_execution_context_struct *ec, struct rb_fiber_struct *fiber); diff --git a/thread.c b/thread.c index 479f703b487da9..7690f3f712976d 100644 --- a/thread.c +++ b/thread.c @@ -5342,6 +5342,7 @@ rb_thread_atfork_internal(rb_thread_t *th, void (*atfork)(rb_thread_t *, const r rb_gc_zombie_objspaces_atfork(); rb_gc_atfork_global_locks(); rb_generic_fields_lock_atfork(); + rb_fiber_pool_lock_atfork(); ccan_list_head_init(&th->interrupt_exec_tasks); vm->fork_gen++; diff --git a/vm_core.h b/vm_core.h index 44e19a5463f1d6..9723d1151ff139 100644 --- a/vm_core.h +++ b/vm_core.h @@ -1722,11 +1722,16 @@ VM_ENV_BOX_UNCHECKED(const VALUE *ep) #if VM_CHECK_MODE > 0 int rb_vm_ep_in_heap_p(const VALUE *ep); #endif +static inline rb_execution_context_t * rb_current_execution_context(bool expect_ec); static inline int VM_ENV_ESCAPED_P(const VALUE *ep) { - VM_ASSERT(rb_vm_ep_in_heap_p(ep) == !!VM_ENV_FLAGS(ep, VM_ENV_FLAG_ESCAPED)); +#if VM_CHECK_MODE > 0 + if (rb_current_execution_context(false)) { + VM_ASSERT(rb_vm_ep_in_heap_p(ep) == !!VM_ENV_FLAGS(ep, VM_ENV_FLAG_ESCAPED)); + } +#endif return VM_ENV_FLAGS(ep, VM_ENV_FLAG_ESCAPED) ? 1 : 0; } From ad23fd4a519fde4fc96c7326fb9b0347cbc347b8 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 22 Sep 2026 20:33:19 +0900 Subject: [PATCH 04/31] [Bug #22340] Fix stat on symbolic-link loops on Windows Preserve `ELOOP` instead of hiding symbolic-link resolution errors with the file-enumeration fallback, so existence checks return false. --- test/ruby/test_file_exhaustive.rb | 15 +++++++++++++++ win32/win32.c | 8 ++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_file_exhaustive.rb b/test/ruby/test_file_exhaustive.rb index 2e227b797f7fa9..90127716c60873 100644 --- a/test/ruby/test_file_exhaustive.rb +++ b/test/ruby/test_file_exhaustive.rb @@ -301,6 +301,21 @@ def test_stat_dotted_prefix end end if NTFS + def test_stat_symlink_loop + return unless symlinkfile + path = make_tmp_filename("symlink_loop") + File.symlink(File.basename(path), path) + assert_predicate(File.lstat(path), :symlink?) + assert_raise(Errno::ELOOP) { File.stat(path) } + end + + def test_exist_p_symlink_loop + return unless symlinkfile + path = make_tmp_filename("symlink_loop") + File.symlink(File.basename(path), path) + assert_file.not_exist?(path) + end + def test_lstat return unless symlinkfile assert_equal(false, File.stat(symlinkfile).symlink?) diff --git a/win32/win32.c b/win32/win32.c index e4c27472a805d4..fd9c86ea1dd544 100644 --- a/win32/win32.c +++ b/win32/win32.c @@ -6070,8 +6070,12 @@ winnt_stat(const WCHAR *path, struct stati128 *st, BOOL lstat) } } else { - if ((open_error == ERROR_FILE_NOT_FOUND) || (open_error == ERROR_INVALID_NAME) - || (open_error == ERROR_PATH_NOT_FOUND || (open_error == ERROR_BAD_NETPATH))) { + switch (open_error) { + case ERROR_FILE_NOT_FOUND: + case ERROR_INVALID_NAME: + case ERROR_PATH_NOT_FOUND: + case ERROR_BAD_NETPATH: + case ERROR_CANT_RESOLVE_FILENAME: errno = map_errno(open_error); return -1; } From 9ab645992d5338ff4dfd3600784590214d808bcb Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 21 Sep 2026 23:50:51 +0900 Subject: [PATCH 05/31] [ruby/fileutils] Add file comparison tests and verify aliases Cover equal and different file contents in `test_cmp` and verify that `compare_file` and `identical?` remain aliases. Fix https://github.com/ruby/fileutils/pull/180 Fix https://github.com/ruby/fileutils/pull/184 https://github.com/ruby/fileutils/commit/01ac124ce4 --- test/fileutils/test_fileutils.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index 27511c2f50d0ea..b9dad6b0081520 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -309,6 +309,15 @@ def test_cmp TARGETS.each do |fname| assert cmp(fname, fname), 'not same?' end + + File.write('tmp/same', "contents\n") + File.write('tmp/copy', "contents\n") + File.write('tmp/different', "content!\n") + File.write('tmp/shorter', "content") + assert(cmp('tmp/same', 'tmp/copy')) + assert_equal(false, cmp('tmp/same', 'tmp/different')) + assert_equal(false, cmp('tmp/same', 'tmp/shorter')) + assert_raise(ArgumentError) { cmp TARGETS[0], TARGETS[0], :undefinedoption => true } @@ -1885,7 +1894,7 @@ def test_remove_dir_with_file def test_compare_file check_singleton :compare_file - # FIXME + assert_equal(FileUtils.method(:cmp), FileUtils.method(:compare_file)) end def test_compare_stream @@ -1973,6 +1982,7 @@ def test_getwd def test_identical? check_singleton :identical? + assert_equal(FileUtils.method(:cmp), FileUtils.method(:identical?)) end def test_link From 54f5107fa78f401096a92acd3168efeb8f56c9f5 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 21 Sep 2026 19:32:02 +0900 Subject: [PATCH 06/31] [ruby/fileutils] Add tests for `FileUtils.compare_stream` Fix https://github.com/ruby/fileutils/pull/181 https://github.com/ruby/fileutils/commit/4b041318e9 --- test/fileutils/test_fileutils.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index b9dad6b0081520..7eb49ad6ec835b 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -1899,7 +1899,10 @@ def test_compare_file def test_compare_stream check_singleton :compare_stream - # FIXME + + assert(FileUtils.compare_stream(StringIO.new("contents\n"), StringIO.new("contents\n"))) + assert_not_equal(true, FileUtils.compare_stream(StringIO.new("contents\n"), StringIO.new("content!\n"))) + assert_not_equal(true, FileUtils.compare_stream(StringIO.new("contents\n"), StringIO.new("content"))) end class Stream From cd35f4b6a3b7a924aa15020ef61ad80b10372149 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 22 Sep 2026 19:45:55 +0900 Subject: [PATCH 07/31] [ruby/fileutils] Test directory changes and verify the `chdir` alias Cover directory changes, block behavior, and verbose output through `cd`, and verify that `chdir` remains an alias. Fix https://github.com/ruby/fileutils/pull/182 https://github.com/ruby/fileutils/commit/8476509760 --- test/fileutils/test_fileutils.rb | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index 7eb49ad6ec835b..c3a916db06fcf7 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -1946,30 +1946,44 @@ def test_uptodate? def test_cd check_singleton :cd + + original = FileUtils.pwd + FileUtils.cd('tmp') + assert_equal(File.join(original, 'tmp'), FileUtils.pwd) + ensure + FileUtils.cd(original) if original end def test_cd_result assert_equal 42, cd('.') { 42 } + + original = FileUtils.pwd + assert_equal('tmp', FileUtils.cd('tmp') {|dir| + assert_equal(File.join(original, 'tmp'), Dir.pwd) + dir + }) + assert_equal(original, FileUtils.pwd) end def test_chdir check_singleton :chdir + assert_equal(FileUtils.method(:cd), FileUtils.method(:chdir)) end - def test_chdir_verbose + def test_cd_verbose assert_output_lines(["cd .", "cd -"], FileUtils) do - FileUtils.chdir('.', verbose: true){} + FileUtils.cd('.', verbose: true){} end end - def test_chdir_verbose_frozen + def test_cd_verbose_frozen o = Object.new o.extend(FileUtils) - o.singleton_class.send(:public, :chdir) + o.singleton_class.send(:public, :cd) o.freeze orig_stdout = $stdout $stdout = StringIO.new - o.chdir('.', verbose: true){} + o.cd('.', verbose: true){} $stdout.rewind assert_equal(<<-END, $stdout.read) cd . From b4188315ef6f1d9a260ce13cbd1f3c7220b56dcd Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 22 Sep 2026 19:45:55 +0900 Subject: [PATCH 08/31] [ruby/fileutils] Verify `getwd` as an alias of `pwd` Avoid duplicating working-directory coverage already in `test_pwd`. Fix https://github.com/ruby/fileutils/pull/183 https://github.com/ruby/fileutils/commit/d937a3d5ba --- test/fileutils/test_fileutils.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index c3a916db06fcf7..d717c34ec28e51 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -1995,6 +1995,7 @@ def test_cd_verbose_frozen def test_getwd check_singleton :getwd + assert_equal(FileUtils.method(:pwd), FileUtils.method(:getwd)) end def test_identical? From e657f57fd3700089525ae5c4fa57af337a54f48b Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 22 Sep 2026 19:45:55 +0900 Subject: [PATCH 09/31] [ruby/fileutils] Test hard-link identity and verify the `link` alias Verify that `ln` creates hard links and that `link` remains an alias. Fix https://github.com/ruby/fileutils/pull/185 https://github.com/ruby/fileutils/commit/46db9d557b --- test/fileutils/test_fileutils.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index d717c34ec28e51..857556fe8720cc 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -937,6 +937,8 @@ def test_ln TARGETS.each do |fname| ln fname, 'tmp/lndest' assert_same_file fname, 'tmp/lndest' + assert_same_entry fname, 'tmp/lndest' + assert_file.identical?(fname, 'tmp/lndest') File.unlink 'tmp/lndest' end @@ -2005,6 +2007,7 @@ def test_identical? def test_link check_singleton :link + assert_equal(FileUtils.method(:ln), FileUtils.method(:link)) end def test_makedirs From 142a7c45c6a9835906e4f0b43a6069a38cdaa8af Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 22 Sep 2026 19:45:55 +0900 Subject: [PATCH 10/31] [ruby/fileutils] Test directory creation results and verify aliases Check `mkdir_p` return values and verify the `makedirs` and `mkpath` aliases without duplicating directory creation tests. Fix https://github.com/ruby/fileutils/pull/186 https://github.com/ruby/fileutils/commit/72ff748e26 --- test/fileutils/test_fileutils.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index 857556fe8720cc..13c34a7b96b12c 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -1240,7 +1240,7 @@ def test_mkdir_p ) my_rm_rf 'tmpdir' dirs.each do |d| - mkdir_p d + assert_equal([d], mkdir_p(d)) assert_directory d assert_file_not_exist "#{d}/a" assert_file_not_exist "#{d}/b" @@ -2012,10 +2012,12 @@ def test_link def test_makedirs check_singleton :makedirs + assert_equal(FileUtils.method(:mkdir_p), FileUtils.method(:makedirs)) end def test_mkpath check_singleton :mkpath + assert_equal(FileUtils.method(:mkdir_p), FileUtils.method(:mkpath)) end def test_move From 8e05d47401f1cfd2452b6935477af1058d0f1099 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 22 Sep 2026 19:45:55 +0900 Subject: [PATCH 11/31] [ruby/fileutils] Test file moves and verify the `move` alias Check the result and effects of `mv` and verify the `move` alias. Fix https://github.com/ruby/fileutils/pull/187 https://github.com/ruby/fileutils/commit/3bed6e74a1 --- test/fileutils/test_fileutils.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index 13c34a7b96b12c..a111403e44b4ec 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -591,6 +591,11 @@ def test_cp_lr def test_mv check_singleton :mv + File.write('tmp/source', "contents\n") + assert_equal(0, FileUtils.mv('tmp/source', 'tmp/destination')) + assert_file_not_exist('tmp/source') + assert_equal("contents\n", File.read('tmp/destination')) + mkdir 'tmp/dest' TARGETS.each do |fname| cp fname, 'tmp/mvsrc' @@ -2022,6 +2027,7 @@ def test_mkpath def test_move check_singleton :move + assert_equal(FileUtils.method(:mv), FileUtils.method(:move)) end def test_rm_rf From 316b514d844d2ef3057e41f132f8850c55a9d631 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 22 Sep 2026 19:45:55 +0900 Subject: [PATCH 12/31] [ruby/fileutils] Test tree removal and verify the `rmtree` alias Cover removal of existing and missing trees through `rm_rf` and verify the `rmtree` alias. Fix https://github.com/ruby/fileutils/pull/188 https://github.com/ruby/fileutils/commit/07cc858f16 --- test/fileutils/test_fileutils.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index a111403e44b4ec..bbb381a60e93bb 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -2033,6 +2033,12 @@ def test_move def test_rm_rf check_singleton :rm_rf + FileUtils.mkdir_p('tmp/tree/child') + File.write('tmp/tree/child/file', "contents\n") + assert_equal(['tmp/tree'], FileUtils.rm_rf('tmp/tree')) + assert_file_not_exist('tmp/tree') + assert_equal(['tmp/missing'], FileUtils.rm_rf('tmp/missing')) + return if /mswin|mingw/ =~ RUBY_PLATFORM mkdir 'tmpdatadir' @@ -2084,6 +2090,7 @@ def test_rmdir def test_rmtree check_singleton :rmtree + assert_equal(FileUtils.method(:rm_rf), FileUtils.method(:rmtree)) end def test_safe_unlink From 52c1a16c895c6f179968f6b15043eb856ac25d25 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 22 Sep 2026 19:45:55 +0900 Subject: [PATCH 13/31] [ruby/fileutils] Test file removal and verify the `safe_unlink` alias Cover removal of existing and missing files through `rm_f` and verify the `safe_unlink` alias. Fix https://github.com/ruby/fileutils/pull/189 https://github.com/ruby/fileutils/commit/902bcce36b --- test/fileutils/test_fileutils.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index bbb381a60e93bb..647b7d790f2f75 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -690,6 +690,11 @@ def test_rm def test_rm_f check_singleton :rm_f + File.write('tmp/file', "contents\n") + assert_equal(['tmp/file'], FileUtils.rm_f('tmp/file')) + assert_file_not_exist('tmp/file') + assert_equal(['tmp/missing'], FileUtils.rm_f('tmp/missing')) + TARGETS.each do |fname| cp fname, 'tmp/rmsrc' rm_f 'tmp/rmsrc' @@ -2095,6 +2100,7 @@ def test_rmtree def test_safe_unlink check_singleton :safe_unlink + assert_equal(FileUtils.method(:rm_f), FileUtils.method(:safe_unlink)) end def test_symlink From c433f9779019a727f58354bd54a5e76f5daeb45b Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 22 Sep 2026 19:45:55 +0900 Subject: [PATCH 14/31] [ruby/fileutils] Test symbolic links and verify the `symlink` alias Cover single, multiple, and dangling links through `ln_s`, keeping valid-link tests available where dangling links are unsupported. Fix https://github.com/ruby/fileutils/pull/190 https://github.com/ruby/fileutils/commit/189aefb679 --- test/fileutils/test_fileutils.rb | 34 ++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index 647b7d790f2f75..4c29d9fa093046 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -1002,9 +1002,24 @@ def test_ln_pathname def test_ln_s check_singleton :ln_s - ln_s TARGETS, 'tmp' - each_srcdest do |fname, lnfname| + TARGETS.each do |fname| + fname = "../#{fname}" + lnfname = 'tmp/lnsdest' + assert_equal(0, ln_s(fname, lnfname)) + assert_file.symlink?(lnfname) assert_equal fname, File.readlink(lnfname) + assert_file.exist?(lnfname) + ensure + rm_f lnfname + end + end if have_symlink? + + def test_ln_s_multiple + ln_s TARGETS.map {|fname| "../#{fname}" }, 'tmp' + each_srcdest do |fname, lnfname| + assert_file.symlink?(lnfname) + assert_equal "../#{fname}", File.readlink(lnfname) + assert_file.exist?(lnfname) ensure rm_f lnfname end @@ -1014,17 +1029,7 @@ def test_ln_s ln_s TARGETS, lnfname } assert_file.not_exist?(lnfname) - - TARGETS.each do |fname| - fname = "../#{fname}" - lnfname = 'tmp/lnsdest' - ln_s fname, lnfname - assert_file.symlink?(lnfname) - assert_equal fname, File.readlink(lnfname) - ensure - rm_f lnfname - end - end if have_symlink? and !no_broken_symlink? + end if have_symlink? def test_ln_s_relative_to_symlinked_directory mkdir_p 'tmp/symlink_dir/.dotfiles/zsh' @@ -1064,6 +1069,8 @@ def test_ln_s_broken_symlink ln_s 'symlink', 'tmp/symlink' } assert_symlink 'tmp/symlink' + assert_equal 'symlink', File.readlink('tmp/symlink') + assert_file.not_exist?('tmp/symlink') end if have_symlink? and !no_broken_symlink? def test_ln_s_pathname @@ -2105,6 +2112,7 @@ def test_safe_unlink def test_symlink check_singleton :symlink + assert_equal(FileUtils.method(:ln_s), FileUtils.method(:symlink)) end def test_touch From a809b598b6cfd098e3760d5f108e9ef43d8baf22 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 22 Sep 2026 20:07:17 +0900 Subject: [PATCH 15/31] [ruby/fileutils] Use a missing target in the dangling symlink test Windows reports self-referential links as existing, so use a missing target to test a dangling link rather than a symbolic-link loop. https://github.com/ruby/fileutils/commit/803fe3be07 --- test/fileutils/test_fileutils.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index 4c29d9fa093046..8d957175f647a6 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -1066,10 +1066,10 @@ def test_ln_s_relative_to_symlinked_directory def test_ln_s_broken_symlink assert_nothing_raised { - ln_s 'symlink', 'tmp/symlink' + ln_s 'missing', 'tmp/symlink' } assert_symlink 'tmp/symlink' - assert_equal 'symlink', File.readlink('tmp/symlink') + assert_equal 'missing', File.readlink('tmp/symlink') assert_file.not_exist?('tmp/symlink') end if have_symlink? and !no_broken_symlink? From ca76cabbb5f7d51fa9dd9e153732925f6d7cdaa2 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 21 Sep 2026 19:36:35 +0900 Subject: [PATCH 16/31] [ruby/fileutils] Add tests for `FileUtils.collect_method` Fix https://github.com/ruby/fileutils/pull/191 https://github.com/ruby/fileutils/commit/ff78cfd3a4 --- test/fileutils/test_fileutils.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index 8d957175f647a6..8feca92df0f979 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -2151,6 +2151,10 @@ def test_touch_mtime end def test_collect_methods + assert_include(FileUtils.collect_method(:preserve), 'cp') + assert_include(FileUtils.collect_method(:preserve), 'install') + assert_include(FileUtils.collect_method(:secure), 'mv') + assert_not_include(FileUtils.collect_method(:secure), 'cp') end def test_commands From ca5017558194ddcf39a5016bfb75042f413be76e Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 21 Sep 2026 19:36:45 +0900 Subject: [PATCH 17/31] [ruby/fileutils] Add tests for `FileUtils.commands` Fix https://github.com/ruby/fileutils/pull/192 https://github.com/ruby/fileutils/commit/df83e4bb04 --- test/fileutils/test_fileutils.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index 8feca92df0f979..9dcaf3d026c6dd 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -2158,6 +2158,10 @@ def test_collect_methods end def test_commands + commands = FileUtils.commands + assert_include(commands, 'mv') + assert_include(commands, 'chdir') + assert_equal(commands.uniq, commands) end def test_have_option? From 7a34d74cf7329ff1ab2a0da1b1dbd2859ee0e630 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 21 Sep 2026 19:36:54 +0900 Subject: [PATCH 18/31] [ruby/fileutils] Add tests for `FileUtils.have_option?` Fix https://github.com/ruby/fileutils/pull/193 https://github.com/ruby/fileutils/commit/6fd04f3cea --- test/fileutils/test_fileutils.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index 9dcaf3d026c6dd..e9bc105302c533 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -2165,6 +2165,10 @@ def test_commands end def test_have_option? + assert(FileUtils.have_option?(:mv, :force)) + assert(FileUtils.have_option?('mv', :secure)) + assert_not_equal(true, FileUtils.have_option?(:mv, :preserve)) + assert_raise(ArgumentError) {FileUtils.have_option?(:missing, :noop)} end def test_options From 6a2c8323748d469bbef007c0858b88002f5bda78 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 21 Sep 2026 19:36:58 +0900 Subject: [PATCH 19/31] [ruby/fileutils] Add tests for `FileUtils.options` Fix https://github.com/ruby/fileutils/pull/194 https://github.com/ruby/fileutils/commit/8b1efe473e --- test/fileutils/test_fileutils.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index e9bc105302c533..b4829b507e80fe 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -2172,6 +2172,10 @@ def test_have_option? end def test_options + options = FileUtils.options + assert_include(options, 'force') + assert_include(options, 'verbose') + assert_equal(options.uniq, options) end def test_options_of From cec5c97bf2bc9caff7fc4f353385824c32bd039d Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 21 Sep 2026 19:37:02 +0900 Subject: [PATCH 20/31] [ruby/fileutils] Add tests for `FileUtils.options_of` Fix https://github.com/ruby/fileutils/pull/195 https://github.com/ruby/fileutils/commit/b2d9b72739 --- test/fileutils/test_fileutils.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index b4829b507e80fe..dd2ba5db3ab41b 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -2179,6 +2179,8 @@ def test_options end def test_options_of + assert_equal(%w[force noop verbose secure], FileUtils.options_of(:mv)) + assert_equal(%w[force noop verbose secure], FileUtils.options_of('mv')) end end From 720994eb1050410e022722fec2a165c69ce18373 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 22 Sep 2026 11:08:33 -0400 Subject: [PATCH 21/31] ZJIT: Fold Class#superclass (#18682) I saw that YJIT has a fast-path for this so I figured we should too. --- zjit.c | 9 ++ zjit/bindgen/src/main.rs | 2 + zjit/src/cruby_bindings.inc.rs | 2 + zjit/src/cruby_methods.rs | 17 +++ zjit/src/hir/opt_tests.rs | 236 +++++++++++++++++++++++++++++++++ 5 files changed, 266 insertions(+) diff --git a/zjit.c b/zjit.c index 4e44febf72b6bc..fcd3f8b51f1cd7 100644 --- a/zjit.c +++ b/zjit.c @@ -345,6 +345,15 @@ rb_zjit_class_initialized_p(VALUE klass) return RCLASS_INITIALIZED_P(klass); } +// Whether rb_class_superclass can be called on the class without raising: it raises +// TypeError when the superclasses array is unbuilt (an uninitialized class, e.g. +// Class.allocate), except for BasicObject, which it special-cases to return nil. +bool +rb_zjit_can_load_superclass_p(VALUE klass) +{ + return klass == rb_cBasicObject || RCLASS_SUPERCLASSES(klass) != NULL; +} + rb_alloc_func_t rb_zjit_class_get_alloc_func(VALUE klass); VALUE rb_class_allocate_instance(VALUE klass); diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 7da19ed0fa92c2..7c312aaf72c254 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -169,6 +169,7 @@ fn main() { .allowlist_function("rb_singleton_class") .allowlist_function("rb_define_class") .allowlist_function("rb_class_get_superclass") + .allowlist_function("rb_class_superclass") .allowlist_function("rb_gc_disable") .allowlist_function("rb_gc_enable") .allowlist_function("rb_gc_mark") @@ -404,6 +405,7 @@ fn main() { .allowlist_function("rb_insn_len") .allowlist_function("rb_yarv_class_of") .allowlist_function("rb_zjit_class_initialized_p") + .allowlist_function("rb_zjit_can_load_superclass_p") .allowlist_function("rb_zjit_class_has_default_allocator") .allowlist_function("rb_zjit_class_get_alloc_func") .allowlist_function("rb_get_ec_cfp") diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 94d7bd4f24bfe5..024380b2368699 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2279,6 +2279,7 @@ unsafe extern "C" { pub fn rb_obj_frozen_p(obj: VALUE) -> VALUE; pub fn rb_class_real(klass: VALUE) -> VALUE; pub fn rb_class_inherited_p(scion: VALUE, ascendant: VALUE) -> VALUE; + pub fn rb_class_superclass(klass: VALUE) -> VALUE; pub fn rb_backref_get() -> VALUE; pub fn rb_range_new(beg: VALUE, end: VALUE, excl: ::std::os::raw::c_int) -> VALUE; pub fn rb_reg_nth_match(n: ::std::os::raw::c_int, md: VALUE) -> VALUE; @@ -2482,6 +2483,7 @@ unsafe extern "C" { recv: VALUE, ) -> *const rb_callable_method_entry_struct; pub fn rb_zjit_class_initialized_p(klass: VALUE) -> bool; + pub fn rb_zjit_can_load_superclass_p(klass: VALUE) -> bool; pub fn rb_zjit_class_get_alloc_func(klass: VALUE) -> rb_alloc_func_t; pub fn rb_zjit_class_has_default_allocator(klass: VALUE) -> bool; pub fn rb_vm_get_untagged_block_handler(reg_cfp: *mut rb_control_frame_t) -> VALUE; diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 4dcd7ca71854c3..5729e27ede3bd8 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -250,6 +250,7 @@ pub fn init() -> Annotations { annotate!(rb_cBasicObject, "!=", inline_basic_object_neq, types::BoolExact); annotate!(rb_cBasicObject, "initialize", inline_basic_object_initialize); annotate!(rb_cClass, "allocate", inline_class_allocate); + annotate!(rb_cClass, "superclass", inline_class_superclass, types::Class.union(types::NilClass)); annotate!(rb_cInteger, "succ", inline_integer_succ); annotate!(rb_cInteger, "^", inline_integer_xor); annotate!(rb_cInteger, "==", inline_integer_eq); @@ -920,6 +921,22 @@ fn inline_class_allocate(fun: &mut hir::Function, block: hir::BlockId, recv: hir fun.try_inline_object_alloc(block, recv, state) } +fn inline_class_superclass(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { + // Class#superclass takes no arguments; calls with the wrong argc bail out with + // ArgcParamMismatch before inlining is attempted. + debug_assert!(args.is_empty(), "Class#superclass takes no arguments"); + // A class's superclass cannot change after the class is created (prepending a module only + // inserts ICLASSes, which superclass skips), so fold the lookup when the receiver is a + // compile-time constant. + let recv_class = fun.type_of(recv).ruby_object()?; + if !unsafe { RB_TYPE_P(recv_class, RUBY_T_CLASS) } { return None; } + // rb_class_superclass raises TypeError on an uninitialized class (e.g. from Class.allocate); + // don't fold. + if !unsafe { rb_zjit_can_load_superclass_p(recv_class) } { return None; } + let superclass = unsafe { rb_class_superclass(recv_class) }; + Some(fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(superclass) })) +} + fn inline_basic_object_initialize(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { if !args.is_empty() { return None; } let result = fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(Qnil) }); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index fb4d338f387b5f..4f738c88c7385c 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -18145,6 +18145,242 @@ mod hir_opt_tests { "); } + #[test] + fn test_fold_class_superclass() { + eval(r#" + class A; end + class B < A; end + def test = B.superclass + test + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:4: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint StableConstantNames(0x1000, B) + v11:ClassSubclass[B@0x1008] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(Class@0x1010, superclass@0x1018, cme:0x1020) + v22:ClassSubclass[A@0x1048] = Const Value(VALUE(0x1048)) + CheckInterrupts + Return v22 + "); + } + + #[test] + fn test_fold_basic_object_superclass() { + eval(r#" + def test = BasicObject.superclass + test + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint StableConstantNames(0x1000, BasicObject) + v11:ClassSubclass[BasicObject@0x1008] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(Class@0x1010, superclass@0x1018, cme:0x1020) + v22:NilClass = Const Value(nil) + CheckInterrupts + Return v22 + "); + } + + #[test] + fn test_fold_class_superclass_skips_prepended_module() { + eval(r#" + class A; end + module M; end + class B < A + prepend M + end + def test = B.superclass + test + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:7: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint StableConstantNames(0x1000, B) + v11:ClassSubclass[B@0x1008] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(Class@0x1010, superclass@0x1018, cme:0x1020) + v22:ClassSubclass[A@0x1048] = Const Value(VALUE(0x1048)) + CheckInterrupts + Return v22 + "); + } + + #[test] + fn test_fold_singleton_class_superclass() { + eval(r#" + class C; end + C1 = C.new.singleton_class.singleton_class + def test = C1.superclass + test + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:4: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint StableConstantNames(0x1000, C1) + v11:ClassSubclass[Class@0x1008] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(Class@0x1010, superclass@0x1018, cme:0x1020) + v22:ClassSubclass[Class@0x1048] = Const Value(VALUE(0x1048)) + CheckInterrupts + Return v22 + "); + } + + #[test] + fn test_dont_fold_uninitialized_class_superclass() { + eval(r#" + C = Class.allocate + def test = C.superclass + begin; test; rescue TypeError; end + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint StableConstantNames(0x1000, C) + v11:ClassExact[C@0x1008] = Const Value(VALUE(0x1008)) + PatchPoint NoSingletonClass(Class@0x1010) + PatchPoint MethodRedefined(Class@0x1010, superclass@0x1018, cme:0x1020) + v23:NilClass|Class = CCallWithFrame v11, :Class#superclass@0x1048 + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_dont_fold_uninitialized_class_with_included_module_superclass() { + // include sets RCLASS_SUPER to the module's ICLASS but leaves the superclasses array + // unbuilt, so Class#superclass still raises TypeError; make sure we don't fold. + // Call test before the include: interpreted Class#superclass has a (bogus) assertion + // that RCLASS_SUPER is unset whenever the superclasses array is, which aborts dev + // builds after the include. Compilation happens at hir_string time, after it. + eval(r#" + module M; end + C = Class.allocate + def test = C.superclass + begin; test; rescue TypeError; end + C.include M + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:4: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint StableConstantNames(0x1000, C) + v11:ClassExact[C@0x1008] = Const Value(VALUE(0x1008)) + PatchPoint NoSingletonClass(Class@0x1010) + PatchPoint MethodRedefined(Class@0x1010, superclass@0x1018, cme:0x1020) + v23:NilClass|Class = CCallWithFrame v11, :Class#superclass@0x1048 + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_dont_fold_unknown_receiver_superclass() { + eval(r#" + def test(c) = c.superclass + test(String) + test(String) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :c@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :c@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Class@0x1008, superclass@0x1010, cme:0x1018) + v23:ClassSubclass[class_exact*:Class@VALUE(0x1008)] = GuardType v10, ClassSubclass[class_exact*:Class@VALUE(0x1008)] recompile + v24:NilClass|Class = CCallWithFrame v23, :Class#superclass@0x1040 + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_fold_profiled_receiver_class_superclass() { + eval(r#" + def test(o) = o.class.superclass + test("abc") + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint NoSingletonClass(String@0x1008) + PatchPoint MethodRedefined(String@0x1008, class@0x1010, cme:0x1018) + v25:StringExact = GuardType v10, StringExact recompile + v26:ClassSubclass[String@0x1008] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(Class@0x1040, superclass@0x1048, cme:0x1050) + v30:ClassSubclass[Object@0x1078] = Const Value(VALUE(0x1078)) + CheckInterrupts + Return v30 + "); + } + #[test] fn test_print_nil_module_name() { eval(r#" From 8d4efc14959b1af3a24e9a8c2b9b5539daff446d Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 22 Sep 2026 12:36:00 -0400 Subject: [PATCH 22/31] ZJIT: Elide Kernel#dup for more classes (#18872) `rb_obj_dup` skips immediates: ```c VALUE rb_obj_dup(VALUE obj) { VALUE dup; if (special_object_p(obj)) { return obj; } // ... } static inline int special_object_p(VALUE obj) { if (SPECIAL_CONST_P(obj)) return TRUE; switch (BUILTIN_TYPE(obj)) { case T_BIGNUM: case T_FLOAT: case T_SYMBOL: case T_RATIONAL: case T_COMPLEX: /* not a comprehensive list */ return TRUE; default: return FALSE; } } ``` --- zjit/src/cruby_methods.rs | 28 ++++ zjit/src/hir/opt_tests.rs | 328 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 356 insertions(+) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 5729e27ede3bd8..99715290e495b2 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -245,6 +245,7 @@ pub fn init() -> Annotations { annotate!(rb_cNilClass, "nil?", inline_nilclass_nil_p); annotate!(rb_mKernel, "nil?", inline_kernel_nil_p); annotate!(rb_mKernel, "respond_to?", inline_kernel_respond_to_p); + annotate!(rb_mKernel, "dup", inline_kernel_dup); annotate!(rb_cBasicObject, "==", inline_basic_object_eq, types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cBasicObject, "!", inline_basic_object_not, types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cBasicObject, "!=", inline_basic_object_neq, types::BoolExact); @@ -1083,6 +1084,33 @@ fn inline_kernel_respond_to_p( Some(fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(result) })) } +fn inline_kernel_dup(fun: &mut hir::Function, _block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { + let &[] = args else { return None; }; + // rb_obj_dup skips the call for "special objects". We don't check for + // bignum/float/rational/complex here because Numeric#dup defines its own no-op `dup` method. + // + // static inline int + // special_object_p(VALUE obj) + // { + // if (SPECIAL_CONST_P(obj)) return TRUE; + // switch (BUILTIN_TYPE(obj)) { + // case T_BIGNUM: + // case T_FLOAT: + // case T_SYMBOL: + // case T_RATIONAL: + // case T_COMPLEX: + // /* not a comprehensive list */ + // return TRUE; + // default: + // return FALSE; + // } + // } + if fun.is_a(recv, types::Immediate.union(types::DynamicSymbol)) { + return Some(recv); + } + None +} + fn inline_kernel_class(fun: &mut hir::Function, block: hir::BlockId, _recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { let &[recv] = args else { return None; }; let recv_class = fun.type_of(recv).runtime_exact_ruby_class()?; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 4f738c88c7385c..502a754bf5ac3c 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -18381,6 +18381,334 @@ mod hir_opt_tests { "); } + #[test] + fn test_elide_kernel_dup_fixnum() { + eval(r#" + def test(o) = o.dup + test(3) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Integer@0x1008, dup@0x1010, cme:0x1018) + v22:Fixnum = GuardType v10, Fixnum recompile + CheckInterrupts + Return v22 + "); + } + + #[test] + fn test_elide_kernel_dup_bignum() { + eval(r#" + def test(o) = o.dup + test(300000000000000000000000000000000000000000000000000000000000000) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Integer@0x1008, dup@0x1010, cme:0x1018) + v22:Bignum = GuardType v10, Bignum recompile + CheckInterrupts + Return v22 + "); + } + + #[test] + fn test_elide_kernel_dup_nil() { + eval(r#" + def test(o) = o.dup + test(nil) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(NilClass@0x1008, dup@0x1010, cme:0x1018) + v23:NilClass = GuardType v10, NilClass recompile + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_elide_kernel_dup_true() { + eval(r#" + def test(o) = o.dup + test(true) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(TrueClass@0x1008, dup@0x1010, cme:0x1018) + v23:TrueClass = GuardType v10, TrueClass recompile + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_elide_kernel_dup_false() { + eval(r#" + def test(o) = o.dup + test(false) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(FalseClass@0x1008, dup@0x1010, cme:0x1018) + v23:FalseClass = GuardType v10, FalseClass recompile + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_elide_kernel_dup_static_symbol() { + eval(r#" + def test(o) = o.dup + test(:foo) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Symbol@0x1008, dup@0x1010, cme:0x1018) + v23:StaticSymbol = GuardType v10, StaticSymbol recompile + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_elide_kernel_dup_dynamic_symbol() { + eval(r#" + def test(o) = o.dup + test(:"v#{1 + 1}") + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Symbol@0x1008, dup@0x1010, cme:0x1018) + v23:DynamicSymbol = GuardType v10, DynamicSymbol recompile + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_elide_kernel_dup_static_flonum() { + eval(r#" + def test(o) = o.dup + test(0.0) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, dup@0x1010, cme:0x1018) + v22:Flonum = GuardType v10, Flonum recompile + CheckInterrupts + Return v22 + "); + } + + #[test] + fn test_elide_kernel_dup_bigfloat() { + eval(r#" + def test(o) = o.dup + test(3000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, dup@0x1010, cme:0x1018) + v22:HeapFloat = GuardType v10, HeapFloat recompile + CheckInterrupts + Return v22 + "); + } + + #[test] + fn test_elide_kernel_dup_rational() { + eval(r#" + def test(o) = o.dup + test(1.to_r) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint NoSingletonClass(Rational@0x1008) + PatchPoint MethodRedefined(Rational@0x1008, dup@0x1010, cme:0x1018) + v23:NumericSubclass[class_exact:Rational] = GuardType v10, NumericSubclass[class_exact:Rational] recompile + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_elide_kernel_dup_complex() { + eval(r#" + def test(o) = o.dup + test(Complex.rect(3, 4)) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint NoSingletonClass(Complex@0x1008) + PatchPoint MethodRedefined(Complex@0x1008, dup@0x1010, cme:0x1018) + v23:NumericSubclass[class_exact:Complex] = GuardType v10, NumericSubclass[class_exact:Complex] recompile + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_no_elide_kernel_dup_heap_object() { + eval(r#" + def test(o) = o.dup + test(Object.new) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :o@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :o@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint NoSingletonClass(Object@0x1008) + PatchPoint MethodRedefined(Object@0x1008, dup@0x1010, cme:0x1018) + v24:ObjectExact = GuardType v10, ObjectExact recompile + v25:BasicObject = CCallWithFrame v24, :Kernel#dup@0x1040 + CheckInterrupts + Return v25 + "); + } + #[test] fn test_print_nil_module_name() { eval(r#" From bf4ab04656294345642ec3663c5f38e1ebd70f8d Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:05:57 +0200 Subject: [PATCH 23/31] [ruby/prism] Move PRISM_NODISCARD to the correct position, again (https://github.com/ruby/prism/pull/4232) Followup to https://github.com/ruby/prism/commit/ba16ae22561179998e4208e4c37262b47f693c11 I can't run rake `cargo:test`: ``` $ cargo build Compiling ruby-prism-sys v1.9.0 (ruby-prism/rust/ruby-prism-sys) Compiling ruby-prism v1.9.0 (ruby-prism/rust/ruby-prism) warning: ruby-prism-sys@1.9.0: In file included from ruby-prism/rust/ruby-prism-sys/vendor/prism-1.9.0/include/prism/ast.h:22, warning: ruby-prism-sys@1.9.0: from ruby-prism/rust/ruby-prism-sys/vendor/prism-1.9.0/include/prism/internal/static_literals.h:6, warning: ruby-prism-sys@1.9.0: from ruby-prism/rust/ruby-prism-sys/vendor/prism-1.9.0/src/static_literals.c:1: warning: ruby-prism-sys@1.9.0: ruby-prism/rust/ruby-prism-sys/vendor/prism-1.9.0/include/prism/arena.h:28:1: warning: 'nodiscard' attribute ignored [-Wattributes] warning: ruby-prism-sys@1.9.0: 28 | PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_arena_t * pm_arena_new(void); warning: ruby-prism-sys@1.9.0: | ^~~~~~~~~~~~~~~~~~~~~~~ warning: ruby-prism-sys@1.9.0: ruby-prism/rust/ruby-prism-sys/vendor/prism-1.9.0/include/prism/arena.h:28:41: error: expected identifier or '(' before 'pm_arena_t' warning: ruby-prism-sys@1.9.0: 28 | PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_arena_t * pm_arena_new(void); warning: ruby-prism-sys@1.9.0: | ^~~~~~~~~~ error: failed to run custom build command for `ruby-prism-sys v1.9.0 (ruby-prism/rust/ruby-prism-sys)` ``` It expands to `__attribute__((__visibility__("default"))) extern [[nodiscard]] pm_arena_t * pm_arena_new(void);`, which is not valid in that order. https://github.com/ruby/prism/commit/3942928d8f --- lib/prism/ffi.rb | 6 ++++-- prism/arena.h | 2 +- prism/buffer.h | 2 +- prism/options.h | 2 +- prism/parser.h | 2 +- prism/source.h | 12 ++++++------ 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb index a9442bd26e9282..192b7cb52e55dd 100644 --- a/lib/prism/ffi.rb +++ b/lib/prism/ffi.rb @@ -54,16 +54,18 @@ def self.resolve_type(type, callbacks) def self.load_exported_functions_from(header, *functions, callbacks) File.foreach("#{INCLUDE_DIR}/#{header}") do |line| # We only want to attempt to load exported functions. - next unless line.start_with?("PRISM_EXPORTED_FUNCTION ") + next unless line.include?("PRISM_EXPORTED_FUNCTION ") # We only want to load the functions that we are interested in. next unless functions.any? { |function| line.include?(function) } + # Strip leading attributes (PRISM_EXPORTED_FUNCTION, PRISM_NODISCARD, etc.) + line = line.sub(/\A(PRISM_\w+(?:\([^)]*\))?)+/, "") # Strip trailing attributes (PRISM_NODISCARD, PRISM_NONNULL(...), etc.) line = line.sub(/\)(\s+PRISM_\w+(?:\([^)]*\))?)+\s*;/, ");") # Parse the function declaration. - unless /^PRISM_EXPORTED_FUNCTION (?.+) (?\w+)\((?.+)\);$/ =~ line + unless /^(?.+) (?\w+)\((?.+)\);$/ =~ line raise "Could not parse #{line}" end diff --git a/prism/arena.h b/prism/arena.h index e1fa8fc6ad2c52..ecd84b9b8f9726 100644 --- a/prism/arena.h +++ b/prism/arena.h @@ -25,7 +25,7 @@ typedef struct pm_arena_t pm_arena_t; * the caller to free the arena using pm_arena_free when it is no longer * needed. */ -PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_arena_t * pm_arena_new(void); +PRISM_NODISCARD PRISM_EXPORTED_FUNCTION pm_arena_t * pm_arena_new(void); /** * Frees both the held memory and the arena itself. diff --git a/prism/buffer.h b/prism/buffer.h index 24b572d2c3f555..b54e5ac0d2258c 100644 --- a/prism/buffer.h +++ b/prism/buffer.h @@ -24,7 +24,7 @@ typedef struct pm_buffer_t pm_buffer_t; * @returns A pointer to the initialized buffer. The caller is responsible for * freeing the buffer with pm_buffer_free. */ -PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_buffer_t * pm_buffer_new(void); +PRISM_NODISCARD PRISM_EXPORTED_FUNCTION pm_buffer_t * pm_buffer_new(void); /** * Free both the memory held by the buffer and the buffer itself. diff --git a/prism/options.h b/prism/options.h index 71e606bb5d66d1..cf8a430bbcbef6 100644 --- a/prism/options.h +++ b/prism/options.h @@ -114,7 +114,7 @@ static const uint8_t PM_OPTIONS_COMMAND_LINE_X = 0x20; * @returns A new options struct with default values. It is the responsibility * of the caller to free this struct using pm_options_free(). */ -PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_options_t * pm_options_new(void); +PRISM_NODISCARD PRISM_EXPORTED_FUNCTION pm_options_t * pm_options_new(void); /** * Free both the held memory of the given options struct and the struct itself. diff --git a/prism/parser.h b/prism/parser.h index 2c8c4b3a7acdc0..2c843ecf5ea360 100644 --- a/prism/parser.h +++ b/prism/parser.h @@ -33,7 +33,7 @@ typedef struct pm_parser_t pm_parser_t; * @returns The initialized parser. It is the responsibility of the caller to * free the parser with `pm_parser_free()`. */ -PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_parser_t * pm_parser_new(pm_arena_t *arena, const uint8_t *source, size_t size, const pm_options_t *options) PRISM_NONNULL(1); +PRISM_NODISCARD PRISM_EXPORTED_FUNCTION pm_parser_t * pm_parser_new(pm_arena_t *arena, const uint8_t *source, size_t size, const pm_options_t *options) PRISM_NONNULL(1); /** * Free both the memory held by the given parser and the parser itself. diff --git a/prism/source.h b/prism/source.h index 115914392f8ec3..741350bfc7bebe 100644 --- a/prism/source.h +++ b/prism/source.h @@ -73,7 +73,7 @@ typedef enum { * @param length The length of the source data in bytes. * @returns A new source. Aborts on allocation failure. */ -PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_source_t * pm_source_constant_new(const uint8_t *data, size_t length); +PRISM_NODISCARD PRISM_EXPORTED_FUNCTION pm_source_t * pm_source_constant_new(const uint8_t *data, size_t length); /** * Create a new source that wraps existing shared memory. The memory is not @@ -83,7 +83,7 @@ PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_source_t * pm_source_constant_new(con * @param length The length of the source data in bytes. * @returns A new source. Aborts on allocation failure. */ -PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_source_t * pm_source_shared_new(const uint8_t *data, size_t length); +PRISM_NODISCARD PRISM_EXPORTED_FUNCTION pm_source_t * pm_source_shared_new(const uint8_t *data, size_t length); /** * Create a new source that owns its memory. The memory will be freed with @@ -93,7 +93,7 @@ PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_source_t * pm_source_shared_new(const * @param length The length of the source data in bytes. * @returns A new source. Aborts on allocation failure. */ -PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_source_t * pm_source_owned_new(uint8_t *data, size_t length); +PRISM_NODISCARD PRISM_EXPORTED_FUNCTION pm_source_t * pm_source_owned_new(uint8_t *data, size_t length); /** * Create a new source by reading a file into a heap-allocated buffer. @@ -102,7 +102,7 @@ PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_source_t * pm_source_owned_new(uint8_ * @param result Out parameter for the result of the initialization. * @returns A new source, or NULL on error (with result written to out param). */ -PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_source_t * pm_source_file_new(const char *filepath, pm_source_init_result_t *result) PRISM_NONNULL(1, 2); +PRISM_NODISCARD PRISM_EXPORTED_FUNCTION pm_source_t * pm_source_file_new(const char *filepath, pm_source_init_result_t *result) PRISM_NONNULL(1, 2); /** * Create a new source by memory-mapping a file. Falls back to file reading on @@ -117,7 +117,7 @@ PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_source_t * pm_source_file_new(const c * @param result Out parameter for the result of the initialization. * @returns A new source, or NULL on error (with result written to out param). */ -PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_source_t * pm_source_mapped_new(const char *filepath, int open_flags, pm_source_init_result_t *result) PRISM_NONNULL(1, 3); +PRISM_NODISCARD PRISM_EXPORTED_FUNCTION pm_source_t * pm_source_mapped_new(const char *filepath, int open_flags, pm_source_init_result_t *result) PRISM_NONNULL(1, 3); /** * Create a new source by reading from a stream using the provided callbacks. @@ -127,7 +127,7 @@ PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_source_t * pm_source_mapped_new(const * @param feof The function to use to check if the stream is at EOF. * @returns A new source. Aborts on allocation failure. */ -PRISM_EXPORTED_FUNCTION PRISM_NODISCARD pm_source_t * pm_source_stream_new(void *stream, pm_source_stream_fgets_t *fgets, pm_source_stream_feof_t *feof); +PRISM_NODISCARD PRISM_EXPORTED_FUNCTION pm_source_t * pm_source_stream_new(void *stream, pm_source_stream_fgets_t *fgets, pm_source_stream_feof_t *feof); /** * Free the given source and any memory it owns. From e77b7b6f1e3759c849d0bd11106e8acaaac0a725 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 22 Sep 2026 15:52:19 -0400 Subject: [PATCH 24/31] ZJIT: Specialize Struct class .new (#18835) We can use `ObjectAllocClass` for Structs because they are basically a `__members__` ivar lookup for sizing plus the default allocator. Requires #18888 to make sure the constructor does not raise. --- struct.c | 7 +++++++ zjit.c | 3 +++ zjit/bindgen/src/main.rs | 1 + zjit/src/cruby.rs | 4 ++++ zjit/src/cruby_bindings.inc.rs | 1 + zjit/src/hir/opt_tests.rs | 31 +++++++++++++++++++++++++++++++ 6 files changed, 47 insertions(+) diff --git a/struct.c b/struct.c index c1efe715f7c896..1afd1a4ba4cc04 100644 --- a/struct.c +++ b/struct.c @@ -859,6 +859,13 @@ struct_alloc(VALUE klass) } } +// Whether `klass` allocates its instances with struct_alloc. +bool +rb_zjit_class_has_struct_allocator(VALUE klass) +{ + return rb_get_alloc_func(klass) == struct_alloc; +} + VALUE rb_struct_alloc(VALUE klass, VALUE values) { diff --git a/zjit.c b/zjit.c index fcd3f8b51f1cd7..e32af670dd29e4 100644 --- a/zjit.c +++ b/zjit.c @@ -356,6 +356,9 @@ rb_zjit_can_load_superclass_p(VALUE klass) rb_alloc_func_t rb_zjit_class_get_alloc_func(VALUE klass); +// Defined in struct.c, where struct_alloc is visible. +bool rb_zjit_class_has_struct_allocator(VALUE klass); + VALUE rb_class_allocate_instance(VALUE klass); bool diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 7c312aaf72c254..c4cb49cefd50c1 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -407,6 +407,7 @@ fn main() { .allowlist_function("rb_zjit_class_initialized_p") .allowlist_function("rb_zjit_can_load_superclass_p") .allowlist_function("rb_zjit_class_has_default_allocator") + .allowlist_function("rb_zjit_class_has_struct_allocator") .allowlist_function("rb_zjit_class_get_alloc_func") .allowlist_function("rb_get_ec_cfp") .allowlist_function("rb_get_cfp_iseq") diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 3d8d561178bb09..1c4302f12e1374 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -1648,6 +1648,10 @@ pub fn class_has_leaf_allocator(class: VALUE) -> bool { if class == unsafe { rb_cString } { return true; } // rb_reg_s_alloc if class == unsafe { rb_cRegexp } { return true; } + // struct_alloc, used by every Struct subclass, is leaf: it reads the hidden __members__ ivar + // and allocates, without calling into Ruby. It does modify the class's __members__ ivar once + // to cache the members, but without a Ractor check. + if unsafe { rb_zjit_class_has_struct_allocator(class) } { return true; } // rb_class_allocate_instance unsafe { rb_zjit_class_has_default_allocator(class) } } diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 024380b2368699..dc0aa8698d362e 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2485,6 +2485,7 @@ unsafe extern "C" { pub fn rb_zjit_class_initialized_p(klass: VALUE) -> bool; pub fn rb_zjit_can_load_superclass_p(klass: VALUE) -> bool; pub fn rb_zjit_class_get_alloc_func(klass: VALUE) -> rb_alloc_func_t; + pub fn rb_zjit_class_has_struct_allocator(klass: VALUE) -> bool; pub fn rb_zjit_class_has_default_allocator(klass: VALUE) -> bool; pub fn rb_vm_get_untagged_block_handler(reg_cfp: *mut rb_control_frame_t) -> VALUE; pub fn rb_vm_once_done_value(is: ISE, result: *mut VALUE) -> bool; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 502a754bf5ac3c..69f27c083613cb 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -12365,6 +12365,37 @@ mod hir_opt_tests { "); } + #[test] + fn test_specialize_struct_new_generates_object_alloc_class() { + eval(r#" + C = Struct.new + def test = C.new + test + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:NilClass = Const Value(nil) + PatchPoint StableConstantNames(0x1000, C) + v13:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) + PatchPoint MethodRedefined(C@0x1008, new@0x1009, cme:0x1010) + v42:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) + PatchPoint NoSingletonClass(C@0x1008) + PatchPoint MethodRedefined(C@0x1008, initialize@0x1038, cme:0x1040) + v47:BasicObject = CCallVariadic v42, :Struct#initialize@0x1068 + CheckInterrupts + Return v42 + "); + } + #[test] fn test_inline_struct_aref_embedded() { eval(r#" From 7e0d49ae490d198413f1752b6f98cc6a4addc7c9 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 22 Sep 2026 17:18:36 -0400 Subject: [PATCH 25/31] ZJIT: Add HIR comment to terminators too (#18890) We previously only had this in `gen_insn` which left terminators un-commented. --- zjit/src/codegen.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 1649c1006e1df7..23f8082c2d0892 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -493,6 +493,7 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func let insn = function.find(insn_id); let symbol_range = perf::hir_symbol_range_start(&mut asm, &insn); + asm_comment!(asm, "Insn: {insn_id} {insn}"); let result = match &insn { Insn::CondBranch { val, if_true, if_false } => { let val_opnd = jit.get_opnd(*val); From dcbf89e0d7a61451596911fe7b17f869f94f623b Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 22 Sep 2026 18:15:22 -0400 Subject: [PATCH 26/31] ZJIT: Make LIR Abort a terminator (#18967) It's an abrupt end to a block, but it definitely ends the block. No need to put a return instruction after it. --- zjit/src/backend/lir.rs | 1 + zjit/src/codegen.rs | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 79d8036451baa1..ebf983315c3200 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -1343,6 +1343,7 @@ impl Insn { self.is_jump() || match self { Insn::CRet(_) => true, + Insn::Abort => true, _ => false } } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 23f8082c2d0892..bedd806435e139 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -2825,10 +2825,8 @@ fn gen_throw(jit: &mut JITState, asm: &mut Assembler, function: &Function, throw } asm_ccall!(asm, rb_zjit_throw, EC, CFP, Opnd::UImm(throw_state.into()), val); - // rb_zjit_throw() never returns. Trap in case it somehow does, and end the - // LIR block with an unreachable ret to give it a normal terminator. + // rb_zjit_throw() never returns. Trap in case it somehow does. asm.abort(); - asm.cret(C_RET_OPND); } /// Compile Fixnum + Fixnum From 59bd0509e5a3b6aa97e3635d4fd437d3ad6af1e1 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 22 Sep 2026 19:20:39 -0400 Subject: [PATCH 27/31] ZJIT: Change HasType into CondBranchHasType (#18633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We use `HasType`+`CondBranch` a lot—it is how we do polymorphic dispatch in HIR—so it's pretty hot. Without smarter instruction fusion/instruction selection, this generates a lot of code. Kokubun identified this in https://github.com/Shopify/ruby/issues/1039 Right now, `HasType` materializes a boolean just for `CondBranch` to go check it again. Since we only use `HasType` for `CondBranch` today, make a new fused instruction. This reduces interstitial values and also intermediate double branching. See for example this polymorphic method dispatch and its generated HIR and asm. Source: ```ruby class C def foo = 3 end class D def foo = 4 end def test(o) = o.foo ``` Before HIR: ``` Optimized HIR: fn test@../tmp/poly.rb:9: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :o@-0x20 Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) v6:BasicObject = LoadArg :self@0 v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): v16:CBool = HasType v10, ObjectSubclass[class_exact:C] CondBranch v16, bb5(), bb6() bb5(): PatchPoint NoSingletonClass(C@0x124574200) PatchPoint MethodRedefined(C@0x124574200, foo@0xec31, cme:0x12455e298) v37:Fixnum[3] = Const Value(3) Jump bb4(v37) bb6(): v22:CBool = HasType v10, ObjectSubclass[class_exact:D] CondBranch v22, bb7(), bb8() bb7(): PatchPoint NoSingletonClass(D@0x124574340) PatchPoint MethodRedefined(D@0x124574340, foo@0xec31, cme:0x12455e2e8) v40:Fixnum[4] = Const Value(4) Jump bb4(v40) bb8(): v28:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic call site Jump bb4(v28) bb4(v15:BasicObject): CheckInterrupts Return v15 ``` Before asm: ``` # Insn: v16 HasType v10, ObjectSubclass[class_exact:C] 0x124f4012c: tst x0, #7 0x124f40130: b.ne #0x124f4016c 0x124f40134: cmp x0, #0 0x124f40138: b.eq #0x124f40164 0x124f4013c: ldur x1, [x0, #8] 0x124f40140: ldr x2, #0x124f40148 0x124f40144: b #0x124f40150 0x124f40148: sel z0.d, p0, z26.d, z16.d 0x124f4014c: udf #1 0x124f40150: cmp x1, x2 0x124f40154: mov x1, #1 0x124f40158: mov x2, #0 0x124f4015c: csel x1, x1, x2, eq 0x124f40160: b #0x124f40170 0x124f40164: mov x1, #0 0x124f40168: b #0x124f40170 0x124f4016c: mov x1, #0 # Insn: v17 CondBranch v16, bb5(), bb6() 0x124f40170: tst x1, x1 0x124f40174: b.ne #0x124f40228 ``` After HIR: ``` Optimized HIR: fn test@../tmp/poly.rb:9: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :o@-0x20 Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) v6:BasicObject = LoadArg :self@0 v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() bb5(): PatchPoint NoSingletonClass(C@0x11fd14140) PatchPoint MethodRedefined(C@0x11fd14140, foo@0xec31, cme:0x11fcfe2f0) v35:Fixnum[3] = Const Value(3) Jump bb4(v35) bb6(): CondBranchHasType v10, ObjectSubclass[class_exact:D], bb7(), bb8() bb7(): PatchPoint NoSingletonClass(D@0x11fd14280) PatchPoint MethodRedefined(D@0x11fd14280, foo@0xec31, cme:0x11fcfe340) v38:Fixnum[4] = Const Value(4) Jump bb4(v38) bb8(): v26:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic call site Jump bb4(v26) bb4(v15:BasicObject): CheckInterrupts Return v15 ``` After asm: ``` # Insn: v16 CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() 0x127e0412c: tst x0, #7 0x127e04130: b.ne #0x127e04168 0x127e04134: cmp x0, #0 0x127e04138: b.eq #0x127e04168 0x127e0413c: ldur x1, [x0, #8] 0x127e04140: ldr x2, #0x127e04148 0x127e04144: b #0x127e04150 0x127e04148: sel z0.d, p0, z25.d, z28.d 0x127e0414c: udf #1 0x127e04150: cmp x1, x2 0x127e04154: b.eq #0x127e0415c 0x127e04158: b #0x127e04168 ``` --- zjit/src/codegen.rs | 111 ++++--- zjit/src/hir.rs | 199 ++++++++++--- zjit/src/hir/opt_tests.rs | 592 ++++++++++++++++++++++---------------- zjit/src/hir/tests.rs | 380 ++++++++++++------------ zjit/src/options.rs | 8 + 5 files changed, 735 insertions(+), 555 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index bedd806435e139..d6525ae3c35289 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -25,7 +25,7 @@ use crate::stats::{counter_ptr, with_time_stat, trace_compile_phase, Counter, Co use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; use crate::backend::lir::{self, Assembler, CArgLocation, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NATIVE_BASE_PTR, NATIVE_STACK_PTR, Opnd, SP, SideExit, SideExitRecompile, SideExitTarget, StackMap, StackMapEntry, Target, asm_ccall, asm_comment}; use crate::hir::{self, iseq_to_hir, BlockId, Invariant, RangeType, SideExitReason::{self, *}, SpecialBackrefSymbol, SpecialObjectType}; -use crate::hir::{BlockHandler, CCallVariadicData, CCallWithFrameData, Const, FieldName, FrameState, Function, Insn, InsnId, Recompile, SendDirectData, SendFallbackReason, qualified_method_name}; +use crate::hir::{BlockHandler, CCallVariadicData, CCallWithFrameData, CondBranchHasTypeData, Const, FieldName, FrameState, Function, Insn, InsnId, Recompile, SendDirectData, SendFallbackReason, qualified_method_name}; use crate::hir_type::{types, Type}; use crate::options::{get_option, InlineDepth, DEFAULT_MAX_VERSIONS}; use crate::cast::IntoUsize; @@ -517,6 +517,26 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func assert!(asm.current_block().insns.last().unwrap().is_terminator()); Ok(()) } + Insn::CondBranchHasType(cond_branch) => { + let CondBranchHasTypeData { val, expected, if_true, if_false } = &**cond_branch; + let val_opnd = jit.get_opnd(*val); + let val_type = function.type_of(*val); + + let true_branch = lir::BranchEdge { + target: hir_to_lir[if_true.target].unwrap(), + args: if_true.args.iter().map(|insn_id| jit.get_opnd(*insn_id)).collect() + }; + + let false_branch = lir::BranchEdge { + target: hir_to_lir[if_false.target].unwrap(), + args: if_false.args.iter().map(|insn_id| jit.get_opnd(*insn_id)).collect() + }; + + gen_cond_branch_has_type(&mut jit, &mut asm, val_opnd, val_type, *expected, true_branch, false_branch); + + assert!(asm.current_block().insns.last().unwrap().is_terminator()); + Ok(()) + } Insn::Jump(target) => { let lir_target = hir_to_lir[target.target].unwrap(); let branch_edge = lir::BranchEdge { @@ -732,10 +752,6 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::UnboxFixnum { val } => gen_unbox_fixnum(asm, opnd!(val)), Insn::Test { val } => gen_test(asm, opnd!(val)), Insn::RefineType { val, .. } => opnd!(val), - Insn::HasType { val, expected } => { - let val_type = function.type_of(*val); - gen_has_type(jit, asm, opnd!(val), val_type, *expected) - } &Insn::GuardType { val, guard_type, state, recompile } => { let val_type = function.type_of(val); gen_guard_type(jit, asm, function, opnd!(val), val_type, guard_type, recompile, &function.frame_state(state)) @@ -805,7 +821,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::ArrayMax { ref elements, state } => gen_array_max(jit, asm, function, opnds!(elements), &function.frame_state(state)), &Insn::ArrayMin { ref elements, state } => gen_array_min(jit, asm, function, opnds!(elements), &function.frame_state(state)), &Insn::Throw { throw_state, val, state } => no_output!(gen_throw(jit, asm, function, throw_state, opnd!(val), &function.frame_state(state))), - &Insn::CondBranch { .. } + &Insn::CondBranch { .. } | &Insn::CondBranchHasType { .. } | &Insn::Jump { .. } | Insn::Entries { .. } => unreachable!(), }; @@ -3039,45 +3055,43 @@ fn gen_test(asm: &mut Assembler, val: lir::Opnd) -> lir::Opnd { asm.csel_e(0.into(), 1.into()) } -fn gen_has_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_type: Type, ty: Type) -> lir::Opnd { - if ty.is_subtype(types::Fixnum) { +/// Branch to `if_true` if `val` has type `ty` and to `if_false` otherwise, using only the +/// condition flags -- the test result is never materialized as a boolean. Terminates the +/// current block. +fn gen_cond_branch_has_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_type: Type, ty: Type, if_true: lir::BranchEdge, if_false: lir::BranchEdge) { + let true_target = Target::Block(Box::new(if_true)); + let false_target = Target::Block(Box::new(if_false)); + + // Each arm sets the flags and picks the conditional jump taken when `val` has type `ty`; + // the not-taken path falls to the unconditional jmp to `false_target` below. + let jcc: fn(Target) -> lir::Insn = if ty.is_subtype(types::Fixnum) { asm.test(val, Opnd::UImm(RUBY_FIXNUM_FLAG as u64)); - asm.csel_nz(Opnd::Imm(1), Opnd::Imm(0)) + lir::Insn::Jnz } else if ty.is_subtype(types::Flonum) { // Flonum: (val & RUBY_FLONUM_MASK) == RUBY_FLONUM_FLAG let masked = asm.and(val, Opnd::UImm(RUBY_FLONUM_MASK as u64)); asm.cmp(masked, Opnd::UImm(RUBY_FLONUM_FLAG as u64)); - asm.csel_e(Opnd::Imm(1), Opnd::Imm(0)) + lir::Insn::Je } else if ty.is_subtype(types::StaticSymbol) { // Static symbols have (val & 0xff) == RUBY_SYMBOL_FLAG // Use 8-bit comparison like YJIT does. // If `val` is a constant (rare but possible), put it in a register to allow masking. let val = asm.load_imm(val); asm.cmp(val.with_num_bits(8), Opnd::UImm(RUBY_SYMBOL_FLAG as u64)); - asm.csel_e(Opnd::Imm(1), Opnd::Imm(0)) + lir::Insn::Je } else if ty.is_subtype(types::NilClass) { asm.cmp(val, Qnil.into()); - asm.csel_e(Opnd::Imm(1), Opnd::Imm(0)) + lir::Insn::Je } else if ty.is_subtype(types::TrueClass) { asm.cmp(val, Qtrue.into()); - asm.csel_e(Opnd::Imm(1), Opnd::Imm(0)) + lir::Insn::Je } else if ty.is_subtype(types::FalseClass) { asm.cmp(val, Qfalse.into()); - asm.csel_e(Opnd::Imm(1), Opnd::Imm(0)) + lir::Insn::Je } else if ty.is_immediate() { // All immediate types' guard should have been handled above panic!("unexpected immediate guard type: {ty}"); } else if let Some(expected_class) = ty.runtime_exact_ruby_class() { - let hir_block_id = asm.current_block().hir_block_id; - let rpo_idx = asm.current_block().rpo_index; - - // Create a result block that all paths converge to - let result_block = asm.new_block(hir_block_id, false, rpo_idx); - let result_edge = |v| Target::Block(Box::new(lir::BranchEdge { - target: result_block, - args: vec![v], - })); - // If val isn't in a register, load it to use it as the base of Opnd::mem later. // TODO: Max thinks codegen should not care about the shapes of the operands except to create them. (Shopify/ruby#685) let val = asm.load_mem(val); @@ -3086,49 +3100,30 @@ fn gen_has_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_typ if !is_known_heap_basic_object { // Immediate -> definitely not the class asm.test(val, (RUBY_IMMEDIATE_MASK as u64).into()); - asm.jnz(jit, result_edge(Opnd::Imm(0))); + asm.jnz(jit, false_target.clone()); // Qfalse -> definitely not the class asm.cmp(val, Qfalse.into()); - asm.je(jit, result_edge(Opnd::Imm(0))); + asm.je(jit, false_target.clone()); } // Heap object -> check klass field let klass = asm.load(Opnd::mem(64, val, RUBY_OFFSET_RBASIC_KLASS)); asm.cmp(klass, Opnd::Value(expected_class)); - let result = asm.csel_e(Opnd::UImm(1), Opnd::Imm(0)); - asm.jmp(result_edge(result)); - - // Result block -- receives the value via block parameter (phi node) - asm.set_current_block(result_block); - let label = jit.get_label(asm, result_block, hir_block_id); - asm.write_label(label); - let param = asm.new_block_param(VALUE_BITS); - asm.current_block().add_parameter(param); - param + lir::Insn::Je } else if let Some(builtin_type) = ty.builtin_type_equivalent() { - let hir_block_id = asm.current_block().hir_block_id; - let rpo_idx = asm.current_block().rpo_index; - - // Create a result block that all paths converge to - let result_block = asm.new_block(hir_block_id, false, rpo_idx); - let result_edge = |v| Target::Block(Box::new(lir::BranchEdge { - target: result_block, - args: vec![v], - })); - // If val isn't in a register, load it to use it as the base of Opnd::mem later. let val = asm.load_mem(val); let is_known_heap_basic_object = val_type.is_subtype(types::HeapBasicObject); if !is_known_heap_basic_object { - // Immediate -> definitely not the class + // Immediate -> definitely not the type asm.test(val, (RUBY_IMMEDIATE_MASK as u64).into()); - asm.jnz(jit, result_edge(Opnd::Imm(0))); + asm.jnz(jit, false_target.clone()); - // Qfalse -> definitely not the class + // Qfalse -> definitely not the type asm.cmp(val, Qfalse.into()); - asm.je(jit, result_edge(Opnd::Imm(0))); + asm.je(jit, false_target.clone()); } // Heap object @@ -3136,19 +3131,13 @@ fn gen_has_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_typ let flags = asm.load(Opnd::mem(VALUE_BITS, val, RUBY_OFFSET_RBASIC_FLAGS)); let tag = asm.and(flags, Opnd::UImm(RUBY_T_MASK as u64)); asm.cmp(tag, Opnd::UImm(builtin_type as u64)); - let result = asm.csel_e(Opnd::UImm(1), Opnd::Imm(0)); - asm.jmp(result_edge(result)); - - // Result block -- receives the value via block parameter (phi node) - asm.set_current_block(result_block); - let label = jit.get_label(asm, result_block, hir_block_id); - asm.write_label(label); - let param = asm.new_block_param(VALUE_BITS); - asm.current_block().add_parameter(param); - param + lir::Insn::Je } else { unimplemented!("unsupported type: {ty}"); - } + }; + + asm.push_insn(jcc(true_target)); + asm.jmp(false_target); } /// Compile a type check with a side exit diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 082a0c57680334..4e8cc031bdb5a4 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -967,6 +967,15 @@ pub struct SendDirectData { pub state: InsnId, } +/// Payload of [`Insn::CondBranchHasType`]. Boxed in the enum to keep `Insn` small. +#[derive(Debug, Clone)] +pub struct CondBranchHasTypeData { + pub val: InsnId, + pub expected: Type, + pub if_true: BranchEdge, + pub if_false: BranchEdge, +} + /// Payload of [`Insn::CCallVariadic`]. Boxed in the enum to keep `Insn` small. #[derive(Debug, Clone)] pub struct CCallVariadicData { @@ -1153,6 +1162,10 @@ pub enum Insn { /// Conditional branch CondBranch { val: InsnId, if_true: BranchEdge, if_false: BranchEdge }, + /// Conditional branch on a type test: branch to if_true if val has type expected and to + /// if_false otherwise. + CondBranchHasType(Box), + /// Call a C function without pushing a frame /// `name` and `owner` are for printing purposes only CCall { cfunc: *const u8, recv: InsnId, args: Vec, name: ID, owner: VALUE, return_type: Type, elidable: bool }, @@ -1305,8 +1318,6 @@ pub enum Insn { /// Refine the known type information of with additional type information. /// Computes the intersection of the existing type and the new type. RefineType { val: InsnId, new_type: Type }, - /// Return CBool[true] if val has type Type and CBool[false] otherwise. - HasType { val: InsnId, expected: Type }, /// Side-exit if val doesn't have the expected type. GuardType { val: InsnId, guard_type: Type, state: InsnId, recompile: Option }, @@ -1456,7 +1467,6 @@ macro_rules! for_each_operand_impl { $visit_one!(*state); } Insn::RefineType { val, .. } - | Insn::HasType { val, .. } | Insn::Return { val } | Insn::Test { val } | Insn::BoxBool { val } => { @@ -1539,6 +1549,11 @@ macro_rules! for_each_operand_impl { $visit_many!(true_args); $visit_many!(false_args); } + Insn::CondBranchHasType(insn) => { + $visit_one!(insn.val); + $visit_many!(insn.if_true.args); + $visit_many!(insn.if_false.args); + } Insn::ArrayDup { val, state } | Insn::Throw { val, state, .. } | Insn::HashDup { val, state } => { @@ -1695,7 +1710,7 @@ impl Insn { Insn::Comment { .. } | Insn::Jump(_) | Insn::Entries { .. } - | Insn::CondBranch { .. } | Insn::EntryPoint { .. } | Insn::Return { .. } + | Insn::CondBranch { .. } | Insn::CondBranchHasType { .. } | Insn::EntryPoint { .. } | Insn::Return { .. } | Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. } | Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetGlobal { .. } | Insn::SetLocal { .. } | Insn::Throw { .. } | Insn::IncrCounter(_) | Insn::IncrCounterPtr { .. } @@ -1710,7 +1725,7 @@ impl Insn { /// Return true if the instruction ends a basic block and false otherwise. pub fn is_terminator(&self) -> bool { match self { - Insn::Unreachable | Insn::CondBranch { .. } | Insn::Jump(_) | Insn::Entries { .. } | Insn::Return { .. } | Insn::SideExit { .. } | Insn::Throw { .. } => true, + Insn::Unreachable | Insn::CondBranch { .. } | Insn::CondBranchHasType { .. } | Insn::Jump(_) | Insn::Entries { .. } | Insn::Return { .. } | Insn::SideExit { .. } | Insn::Throw { .. } => true, _ => false, } } @@ -1718,7 +1733,7 @@ impl Insn { /// Return true if the instruction is a jump (has successor blocks in the CFG). pub fn is_jump(&self) -> bool { match self { - Insn::CondBranch { .. } | Insn::Jump(_) | Insn::Entries { .. } => true, + Insn::CondBranch { .. } | Insn::CondBranchHasType { .. } | Insn::Jump(_) | Insn::Entries { .. } => true, _ => false, } } @@ -1847,6 +1862,11 @@ impl Insn { Insn::Snapshot { .. } => effects::Empty, Insn::Jump(_) => effects::Any, Insn::CondBranch { .. } => effects::Any, + Insn::CondBranchHasType(insn) + => Effect::read_write( + if insn.expected.is_subtype(types::Immediate) { abstract_heaps::Empty } else { abstract_heaps::Memory }, + abstract_heaps::Control + ), Insn::CCall { elidable, .. } => { if *elidable { Effect::write(abstract_heaps::Allocator) @@ -1940,11 +1960,6 @@ impl Insn { Insn::InvokeProc { .. } => effects::Any, Insn::InvokeBlockIseqDirect { .. } => effects::Any, Insn::RefineType { .. } => effects::Empty, - Insn::HasType { expected, .. } - => Effect::read_write( - if expected.is_subtype(types::Immediate) { abstract_heaps::Empty } else { abstract_heaps::Memory }, - abstract_heaps::Empty - ), Insn::Entries { .. } => effects::Any, Insn::BreakPoint | Insn::Unreachable => Effect::read_write(abstract_heaps::Empty, abstract_heaps::Control), } @@ -2202,6 +2217,10 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::FixnumAref { recv, index } => write!(f, "FixnumAref {recv}, {index}"), Insn::Jump(target) => { write!(f, "Jump {target}") } Insn::CondBranch { val, if_true, if_false } => { write!(f, "CondBranch {val}, {if_true}, {if_false}") }, + Insn::CondBranchHasType(insn) => { + let CondBranchHasTypeData { val, expected, if_true, if_false } = &**insn; + write!(f, "CondBranchHasType {val}, {}, {if_true}, {if_false}", expected.print(self.ptr_map)) + }, Insn::SendDirect(insn) => { let SendDirectData { recv, cme, iseq, args, block, jit_entry_idx, .. } = &**insn; let blockiseq = block.map(|bh| match bh { BlockHandler::BlockIseq(iseq) => iseq, BlockHandler::BlockArg => unreachable!() }); @@ -2324,7 +2343,6 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { return Ok(()) }, Insn::RefineType { val, new_type, .. } => { write!(f, "RefineType {val}, {}", new_type.print(self.ptr_map)) }, - Insn::HasType { val, expected, .. } => { write!(f, "HasType {val}, {}", expected.print(self.ptr_map)) }, Insn::GuardBitEquals { val, expected, recompile, .. } => { write!(f, "GuardBitEquals {val}, {}", expected.print(self.ptr_map))?; if recompile.is_some() { @@ -3428,6 +3446,7 @@ impl Function { let (first, second, rest): (Option, Option, &[BlockId]) = match terminator { Insn::CondBranch { if_true, if_false, .. } => (Some(if_true.target), Some(if_false.target), &[]), + Insn::CondBranchHasType(insn) => (Some(insn.if_true.target), Some(insn.if_false.target), &[]), Insn::Jump(edge) => (Some(edge.target), None, &[]), Insn::Entries { targets } => (None, None, targets.as_slice()), @@ -3641,7 +3660,7 @@ impl Function { Insn::LoadArg { val_type, .. } => *val_type, Insn::SetGlobal { .. } | Insn::Jump(_) | Insn::Entries { .. } | Insn::EntryPoint { .. } | Insn::Comment { .. } - | Insn::CondBranch { .. } | Insn::Return { .. } | Insn::Throw { .. } + | Insn::CondBranch { .. } | Insn::CondBranchHasType { .. } | Insn::Return { .. } | Insn::Throw { .. } | Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. } | Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetLocal { .. } | Insn::IncrCounter(_) | Insn::IncrCounterPtr { .. } @@ -3705,9 +3724,6 @@ impl Function { Insn::CheckMatch { .. } => types::BasicObject, Insn::GuardType { val, guard_type, .. } => self.type_of(*val).intersection(*guard_type), Insn::RefineType { val, new_type, .. } => self.type_of(*val).intersection(*new_type), - &Insn::HasType { val, expected } if self.is_a(val, expected) => Type::from_cbool(true), - &Insn::HasType { val, expected } if !self.type_of(val).could_be(expected) => Type::from_cbool(false), - Insn::HasType { .. } => types::CBool, Insn::GuardBitEquals { val, expected, .. } => self.type_of(*val).intersection(Type::from_const(*expected)), Insn::GuardAnyBitSet { val, .. } => self.type_of(*val), Insn::GuardNoBitsSet { val, .. } => self.type_of(*val), @@ -3920,6 +3936,47 @@ impl Function { } continue; } + Insn::CondBranchHasType(insn) => { + let CondBranchHasTypeData { val, expected, if_true, if_false } = &**insn; + let val_type = self.type_of(*val); + // If we're looking at + // CondBranchHasType v, T, if_true, if_false + // then we have four cases: + // + // * if v could be a T, then if_true is reachable + // * if v is a T, then if_false is not reachable + // * if v is not a T, then we don't know anything about if_false and + // have to assume it's reachable + // * if v could not be a T, then if_true is not reachable + // * if v is a T, then v has the Empty type which means neither is reachable + // * if v is not a T, then it has some other non-Empty type that + // overlaps with T and therefore if_false is reachable + // + // If you explode this decision tree out, you end up with the collapsed + // tree of two sequential not-mutually-exclusive checks: + // + // * if v could be a T, then if_true is reachable + // * (not else!) if v is not a T, then if_false is reachable + if val_type.could_be(*expected) { + reachable.insert(if_true.target); + let arg_types: Vec = if_true.args.iter().map(|a| self.type_of(*a)).collect(); + for (idx, arg_type) in arg_types.into_iter().enumerate() { + let param = self.blocks[if_true.target].params[idx]; + changed |= set_type!(param, self.type_of(param).union(arg_type)); + } + traversed_back_edge |= rpo_order[if_true.target] <= rpo_index; + } + if !val_type.is_subtype(*expected) { + reachable.insert(if_false.target); + let arg_types: Vec = if_false.args.iter().map(|a| self.type_of(*a)).collect(); + for (idx, arg_type) in arg_types.into_iter().enumerate() { + let param = self.blocks[if_false.target].params[idx]; + changed |= set_type!(param, self.type_of(param).union(arg_type)); + } + traversed_back_edge |= rpo_order[if_false.target] <= rpo_index; + } + continue; + } &Insn::Jump(BranchEdge { target, ref args }) => { reachable.insert(target); let arg_types: Vec = args.iter().map(|a| self.type_of(*a)).collect(); @@ -6392,11 +6449,14 @@ impl Function { *fun.blocks[block_id].insns().last().unwrap() } + // The extra `&`/`&mut` tokens borrow the boxed CondBranchHasType payload's edges with the + // same mutability as the match ergonomics give the other arms' bindings. macro_rules! edges_of { - ($insn:expr) => { + ($insn:expr, $($borrow:tt)+) => { match $insn { Insn::Jump(edge) => [Some(edge), None], Insn::CondBranch { if_true, if_false, .. } => [Some(if_true), Some(if_false)], + Insn::CondBranchHasType(insn) => [Some($($borrow)+ insn.if_true), Some($($borrow)+ insn.if_false)], _ => [None, None], }.into_iter().flatten() }; @@ -6404,12 +6464,12 @@ impl Function { fn outgoing_edges(fun: &Function, block_id: BlockId) -> impl Iterator { let insn_id = block_terminator(fun, block_id); - edges_of!(&fun.insns[insn_id]) + edges_of!(&fun.insns[insn_id], &) } fn outgoing_edges_mut(fun: &mut Function, block_id: BlockId) -> impl Iterator { let insn_id = block_terminator(fun, block_id); - edges_of!(&mut fun.insns[insn_id]) + edges_of!(&mut fun.insns[insn_id], &mut) } // Instantiate the domain for abstract interpretation. @@ -7001,6 +7061,16 @@ impl Function { &Insn::CondBranch { val, ref if_false, .. } if self.is_a(val, Type::from_cbool(false)) => { self.new_insn(Insn::Jump(if_false.clone())) } + // Match infer_types implementation of CondBranchHasType. + Insn::CondBranchHasType(insn) if self.type_of(insn.val).is_subtype(types::Empty) => { + self.new_insn(Insn::Unreachable) + } + Insn::CondBranchHasType(insn) if self.is_a(insn.val, insn.expected) => { + self.new_insn(Insn::Jump(insn.if_true.clone())) + } + Insn::CondBranchHasType(insn) if !self.type_of(insn.val).could_be(insn.expected) => { + self.new_insn(Insn::Jump(insn.if_false.clone())) + } _ => insn_id, }; // If we're adding a new instruction, mark the two equivalent in the union-find and @@ -7636,6 +7706,10 @@ impl Function { check_edge(block_id, if_true)?; check_edge(block_id, if_false)?; } + Insn::CondBranchHasType(insn) => { + check_edge(block_id, &insn.if_true)?; + check_edge(block_id, &insn.if_false)?; + } _ => {} } @@ -7728,6 +7802,10 @@ impl Function { propagate(if_true.target)?; propagate(if_false.target)?; } + Insn::CondBranchHasType(insn) => { + propagate(insn.if_true.target)?; + propagate(insn.if_false.target)?; + } Insn::Entries { targets } => { for &target in targets { propagate(target)?; @@ -8123,7 +8201,7 @@ impl Function { self.assert_subtype(insn_id, class, types::Class) } Insn::RefineType { .. } => Ok(()), - Insn::HasType { val, .. } => self.assert_subtype(insn_id, val, types::BasicObject), + Insn::CondBranchHasType(ref insn) => self.assert_subtype(insn_id, insn.val, types::BasicObject), Insn::IsBlockParamModified { flags } => self.assert_subtype(insn_id, flags, types::CUInt64), // Frame instructions have no output to validate; their operands // are validated by the recv+args group (PushLightweightFrame) @@ -9561,7 +9639,6 @@ fn add_iseq_to_hir( fun.push_insn(block, Insn::CheckInterrupts { state: exit_id }); } let val = state.stack_pop()?; - let test_id = fun.push_insn(block, Insn::HasType { val, expected: types::NilClass }); let target_idx = insn_idx_at_offset(insn_idx, offset); let target = insn_idx_to_block[&target_idx]; let nil = fun.push_insn(block, Insn::Const { val: Const::Value(Qnil) }); @@ -9570,11 +9647,12 @@ fn add_iseq_to_hir( let fall_through = fun.new_block(insn_idx); - fun.push_insn(block, Insn::CondBranch { - val: test_id, + fun.push_insn(block, Insn::CondBranchHasType(Box::new(CondBranchHasTypeData { + val, + expected: types::NilClass, if_true: BranchEdge { target, args: iftrue_state.as_args(self_param) }, if_false: BranchEdge { target: fall_through, args: vec![] } - }); + }))); block = fall_through; let new_type = types::NotNil; @@ -10083,14 +10161,14 @@ fn add_iseq_to_hir( continue; } seen_types.push(expected); - let has_type = fun.push_insn(block, Insn::HasType { val: recv, expected }); let iftrue_block = fun.new_block(insn_idx); let fall_through = fun.new_block(insn_idx); - fun.push_insn(block, Insn::CondBranch { - val: has_type, + fun.push_insn(block, Insn::CondBranchHasType(Box::new(CondBranchHasTypeData { + val: recv, + expected, if_true: BranchEdge { target: iftrue_block, args: vec![] }, if_false: BranchEdge { target: fall_through, args: vec![] } - }); + }))); block = fall_through; // Take a fresh Snapshot rather than // reusing exit_id so type specialization resolves the receiver from @@ -10160,14 +10238,14 @@ fn add_iseq_to_hir( continue; } seen_types.push(expected); - let has_type = fun.push_insn(block, Insn::HasType { val: recv, expected }); let iftrue_block = fun.new_block(insn_idx); let fall_through = fun.new_block(insn_idx); - fun.push_insn(block, Insn::CondBranch { - val: has_type, + fun.push_insn(block, Insn::CondBranchHasType(Box::new(CondBranchHasTypeData { + val: recv, + expected, if_true: BranchEdge { target: iftrue_block, args: vec![] }, if_false: BranchEdge { target: fall_through, args: vec![] } - }); + }))); block = fall_through; // Take a fresh Snapshot rather than // reusing exit_id so type specialization resolves the receiver from @@ -10633,15 +10711,15 @@ fn add_iseq_to_hir( fun.push_insn(block, Insn::Send { recv, cd, block: None, args: vec![], caller_splat_length: None, state: exit_id, reason: ObjToStringNotString }) } } else { - let has_type = fun.push_insn(block, Insn::HasType { val: recv, expected: types::String }); let iftrue_block = fun.new_block(insn_idx); let iffalse_block = fun.new_block(insn_idx); let join_block = fun.new_block(insn_idx); - fun.push_insn(block, Insn::CondBranch { - val: has_type, + fun.push_insn(block, Insn::CondBranchHasType(Box::new(CondBranchHasTypeData { + val: recv, + expected: types::String, if_true: BranchEdge { target: iftrue_block, args: vec![] }, if_false: BranchEdge { target: iffalse_block, args: vec![] } - }); + }))); // true block let refined = fun.push_insn(iftrue_block, Insn::RefineType { val: recv, new_type: types::String }); fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![refined] })); @@ -10660,15 +10738,15 @@ fn add_iseq_to_hir( let val = state.stack_pop()?; // Mirror logic of rb_obj_as_string_result() (`anytostring` in insns.def) - let has_type = fun.push_insn(block, Insn::HasType { val: str, expected: types::String }); let iftrue_block = fun.new_block(insn_idx); let iffalse_block = fun.new_block(insn_idx); let join_block = fun.new_block(insn_idx); - fun.push_insn(block, Insn::CondBranch { - val: has_type, + fun.push_insn(block, Insn::CondBranchHasType(Box::new(CondBranchHasTypeData { + val: str, + expected: types::String, if_true: BranchEdge { target: iftrue_block, args: vec![] }, if_false: BranchEdge { target: iffalse_block, args: vec![] } - }); + }))); // true block let refined = fun.push_insn(iftrue_block, Insn::RefineType { val: str, new_type: types::String }); fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![refined] })); @@ -11512,6 +11590,47 @@ mod validation_tests { }); } + // A `CondBranchHasType` whose `val` has been refined to a disjoint type is + // `Empty` (Bottom): an impossible value, so the branch is dead. `infer_types` + // uses `could_be` and so marks neither edge reachable (both params stay + // `Empty`), and `fold_constants` folds the branch to `Unreachable` to match. + #[test] + fn condbranchhastype_bottom_val_folds_to_unreachable() { + let mut function = Function::new(std::ptr::null()); + let entry = function.entry_block; + let if_true = function.new_block(0); + let if_false = function.new_block(0); + let p_true = function.push_insn(if_true, Insn::Param); + function.push_insn(if_true, Insn::Return { val: p_true }); + let p_false = function.push_insn(if_false, Insn::Param); + function.push_insn(if_false, Insn::Return { val: p_false }); + // Refine a nil to Fixnum: NilClass ∩ Fixnum = Empty (Bottom). + let nil = function.push_insn(entry, Insn::Const { val: Const::Value(Qnil) }); + let bottom = function.push_insn(entry, Insn::RefineType { val: nil, new_type: types::Fixnum }); + let arg = function.push_insn(entry, Insn::Const { val: Const::Value(VALUE::fixnum_from_usize(3)) }); + function.push_insn(entry, Insn::CondBranchHasType(Box::new(CondBranchHasTypeData { + val: bottom, + expected: types::Fixnum, + if_true: BranchEdge { target: if_true, args: vec![arg] }, + if_false: BranchEdge { target: if_false, args: vec![arg] }, + }))); + function.seal_entries(); + crate::cruby::with_rubyvm(|| { + function.infer_types(); + assert!(function.type_of(bottom).bit_equal(types::Empty), "refine to disjoint type should be Bottom"); + // `could_be`-only means neither edge is reachable, so both params are Empty. + assert!(function.type_of(p_true).bit_equal(types::Empty), + "if_true param should be Empty, got {}", function.type_of(p_true)); + assert!(function.type_of(p_false).bit_equal(types::Empty), + "if_false param should be Empty, got {}", function.type_of(p_false)); + // The dead branch folds to Unreachable, agreeing with infer_types. + function.fold_constants(); + let last = *function.blocks[entry].insns.last().unwrap(); + assert!(matches!(function.find_ref(last), Insn::Unreachable), + "expected entry terminator to fold to Unreachable"); + }); + } + #[test] fn instruction_appears_twice_in_same_block() { let mut function = Function::new(std::ptr::null()); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 69f27c083613cb..544b3c1eaffd2c 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -1858,10 +1858,129 @@ mod hir_opt_tests { v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile PushInlineFrame :m, v20 (0x1038), num_args=1 PatchPoint MethodRedefined(NilClass@0x1058, nil?@0x1060, cme:0x1068) - v84:Fixnum[0] = Const Value(0) + v82:Fixnum[0] = Const Value(0) PopInlineFrame CheckInterrupts - Return v84 + Return v82 + "); + } + + // Regression test for a dead string-interpolation raise block whose receiver is + // refined to Bottom. Across the multi-version recompile, the polymorphic `enc.foo` + // guard funnels the common `mode == nil` path so `mode` freezes to a monomorphic + // NilClass guard upstream, while the rare string-`mode` calls have already frozen + // the `#{mode}` interpolation site as String-profiled. That leaves + // `GuardType(NilClass, String)` = Bottom feeding the CondBranchHasType on the dead + // `raise` block. Without folding that to Unreachable, the dead block survives, + // `#{uri.class}` (Module#to_s -> BasicObject) flows into a StringConcat that requires + // String operands, and `validate()` aborts the process during the driver's own JIT + // compile inside eval. With the fold the block is proved dead and eval completes, + // so hir_string can then compile a valid function. + #[test] + fn test_dead_string_interp_raise_block_folds_to_unreachable() { + // NB: 31, not 30. boot_rubyvm() resets the threshold to 2 whenever it equals + // DEFAULT_CALL_THRESHOLD (30), which would make profiling sample the very first + // (String) call and defeat the race we need. + set_call_threshold(31); + set_num_profiles(1); + set_max_versions(2); + set_inline_threshold(0); + eval(r#" + class FakeURI; end + class A; def foo; 1; end; end + class B; def foo; 2; end; end + + def open_uri(uri, enc, mode) + enc.foo + unless mode == nil || mode == 'r' || mode == 'rb' + raise ArgumentError.new("invalid access mode #{mode} (#{uri.class} resource is read only.)") + end + :ok + end + + u = FakeURI.new + encs = [A.new, B.new] + i = 0 + while i < 3000 + enc = encs[i % 2] + mode = (i % 31 == 0) ? "zz" : nil + begin; open_uri(u, enc, mode); rescue ArgumentError; end + i += 1 + end + :done + "#); + assert_snapshot!(hir_string("open_uri"), @" + fn open_uri@:7: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :uri@0x1000 + v4:BasicObject = LoadField v2, :enc@0x1001 + v5:BasicObject = LoadField v2, :mode@0x1002 + Jump bb3(v1, v3, v4, v5) + bb2(): + EntryPoint JIT(0) + v8:BasicObject = LoadArg :self@0 + v9:BasicObject = LoadArg :uri@1 + v10:BasicObject = LoadArg :enc@2 + v11:BasicObject = LoadArg :mode@3 + Jump bb3(v8, v9, v10, v11) + bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:BasicObject): + CondBranchHasType v15, ObjectSubclass[class_exact:A], bb8(), bb9() + bb8(): + PatchPoint NoSingletonClass(A@0x1008) + PatchPoint MethodRedefined(A@0x1008, foo@0x1010, cme:0x1018) + v167:Fixnum[1] = Const Value(1) + Jump bb7(v167) + bb9(): + CondBranchHasType v15, ObjectSubclass[class_exact:B], bb10(), bb11() + bb10(): + PatchPoint NoSingletonClass(B@0x1040) + PatchPoint MethodRedefined(B@0x1040, foo@0x1010, cme:0x1048) + v170:Fixnum[2] = Const Value(2) + Jump bb7(v170) + bb11(): + v32:BasicObject = Send v15, :foo # SendFallbackReason: Send: polymorphic fallback + Jump bb7(v32) + bb7(v21:BasicObject): + PatchPoint NoEPEscape(open_uri) + v40:NilClass = Const Value(nil) + PatchPoint MethodRedefined(NilClass@0x1070, ==@0x1078, cme:0x1080) + v173:NilClass = GuardType v16, NilClass recompile + v174:CBool = IsBitEqual v173, v40 + CondBranch v174, bb6(), bb12() + bb12(): + v51:StringExact[VALUE(0x10a8)] = Const Value(VALUE(0x10a8)) + v52:StringExact = StringCopy v51 + PatchPoint NoSingletonClass(String@0x10b0) + PatchPoint MethodRedefined(String@0x10b0, ==@0x1078, cme:0x10b8) + v179 = GuardType v173, StringExact recompile + v180:BoolExact = StringEqual v179, v52 + v57:CBool = Test v180 + CondBranch v57, bb6(), bb13() + bb13(): + v63:StringExact[VALUE(0x10e0)] = Const Value(VALUE(0x10e0)) + v64:StringExact = StringCopy v63 + PatchPoint NoSingletonClass(String@0x10b0) + PatchPoint MethodRedefined(String@0x10b0, ==@0x1078, cme:0x10b8) + v184 = GuardType v173, StringExact recompile + v185:BoolExact = StringEqual v184, v64 + v69:CBool = Test v185 + CondBranch v69, bb6(), bb14() + bb6(): + v133:StaticSymbol[:ok] = Const Value(VALUE(0x10e8)) + CheckInterrupts + Return v133 + bb14(): + v76:NilClass = Const Value(nil) + PatchPoint StableConstantNames(0x10f0, ArgumentError) + v79:ClassSubclass[ArgumentError@0x10f8] = Const Value(VALUE(0x10f8)) + v81:StringExact[VALUE(0x1100)] = Const Value(VALUE(0x1100)) + PatchPoint NoEPEscape(open_uri) + PatchPoint NoSingletonClass(String@0x10b0) + v88 = GuardType v173, String + Unreachable "); } @@ -8889,9 +9008,9 @@ mod hir_opt_tests { v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v13:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v14:StringExact = StringCopy v13 - v34:StringExact = StringConcat v10, v14 + v32:StringExact = StringConcat v10, v14 CheckInterrupts - Return v34 + Return v32 "); } @@ -8914,10 +9033,10 @@ mod hir_opt_tests { v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018) - v40:StringExact = CCallVariadic v12, :Integer#to_s@0x1040 - v32:StringExact = StringConcat v10, v40 + v38:StringExact = CCallVariadic v12, :Integer#to_s@0x1040 + v30:StringExact = StringConcat v10, v38 CheckInterrupts - Return v32 + Return v30 "); } @@ -8947,9 +9066,9 @@ mod hir_opt_tests { v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(String@0x1010) v19:String = GuardType v10, String - v29:StringExact = StringConcat v14, v19 + v28:StringExact = StringConcat v14, v19 CheckInterrupts - Return v29 + Return v28 "); } @@ -8982,9 +9101,9 @@ mod hir_opt_tests { v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(MyString@0x1010) v19:String = GuardType v10, String - v29:StringExact = StringConcat v14, v19 + v28:StringExact = StringConcat v14, v19 CheckInterrupts - Return v29 + Return v28 "); } @@ -9015,19 +9134,18 @@ mod hir_opt_tests { v18:ArrayExact = GuardType v10, ArrayExact PatchPoint NoSingletonClass(Array@0x1010) PatchPoint MethodRedefined(Array@0x1010, to_s@0x1018, cme:0x1020) - v39:BasicObject = CCallWithFrame v18, :Array#to_s@0x1048 - v21:CBool = HasType v39, String - CondBranch v21, bb4(), bb5() + v38:BasicObject = CCallWithFrame v18, :Array#to_s@0x1048 + CondBranchHasType v38, String, bb4(), bb5() bb4(): - v23:String = RefineType v39, String - Jump bb6(v23) + v22:String = RefineType v38, String + Jump bb6(v22) bb5(): - v25:StringExact = AnyToString v18 - Jump bb6(v25) - bb6(v27:String): - v29:StringExact = StringConcat v14, v27 + v24:StringExact = AnyToString v18 + Jump bb6(v24) + bb6(v26:String): + v28:StringExact = StringConcat v14, v26 CheckInterrupts - Return v29 + Return v28 "); } @@ -9051,10 +9169,10 @@ mod hir_opt_tests { v5:BasicObject = LoadArg :self@0 Jump bb3(v5) bb3(v8:BasicObject): - v36:NilClass = Const Value(nil) - v20:NilClass = Const Value(nil) + v35:NilClass = Const Value(nil) + v19:NilClass = Const Value(nil) CheckInterrupts - Return v20 + Return v19 "); } @@ -9078,13 +9196,13 @@ mod hir_opt_tests { v5:BasicObject = LoadArg :self@0 Jump bb3(v5) bb3(v8:BasicObject): - v37:NilClass = Const Value(nil) + v36:NilClass = Const Value(nil) v13:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1000, itself@0x1008, cme:0x1010) - v39:Fixnum[1] = Const Value(1) v38:Fixnum[1] = Const Value(1) + v37:Fixnum[1] = Const Value(1) CheckInterrupts - Return v39 + Return v38 "); } @@ -10012,22 +10130,20 @@ mod hir_opt_tests { v27:NilClass = Const Value(nil) Jump bb5(v16, v27) bb5(v30:BasicObject, v31:Falsy): - v36:CBool = HasType v31, FalseClass - CondBranch v36, bb8(), bb9() + CondBranchHasType v31, FalseClass, bb8(), bb9() bb8(): PatchPoint MethodRedefined(FalseClass@0x1008, !@0x1010, cme:0x1018) - v57:TrueClass = Const Value(true) - Jump bb7(v57) + v55:TrueClass = Const Value(true) + Jump bb7(v55) bb9(): - v42:CBool = HasType v31, NilClass - CondBranch v42, bb10(), bb11() + CondBranchHasType v31, NilClass, bb10(), bb11() bb10(): PatchPoint MethodRedefined(NilClass@0x1040, !@0x1010, cme:0x1018) - v60:TrueClass = Const Value(true) - Jump bb7(v60) + v58:TrueClass = Const Value(true) + Jump bb7(v58) bb11(): - v48:BasicObject = Send v31, :! # SendFallbackReason: Send: polymorphic fallback - Jump bb7(v48) + v46:BasicObject = Send v31, :! # SendFallbackReason: Send: polymorphic fallback + Jump bb7(v46) bb7(v35:BasicObject): CheckInterrupts Return v35 @@ -10805,16 +10921,15 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:HeapBasicObject, v10:BasicObject): v17:Fixnum[5] = Const Value(5) - v21:CBool = HasType v10, ObjectSubclass[class_exact:C] - CondBranch v21, bb5(), bb6() + CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() bb5(): - v24:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] + v23:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] PatchPoint MethodRedefined(C@0x1008, foo=@0x1010, cme:0x1018) - SetIvar v24, :@foo, v17 + SetIvar v23, :@foo, v17 Jump bb4(v17) bb6(): - v27:BasicObject = Send v10, :foo=, v17 # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v27) + v26:BasicObject = Send v10, :foo=, v17 # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v26) bb4(v20:BasicObject): CheckInterrupts Return v17 @@ -11471,17 +11586,16 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v16:CBool = HasType v10, ObjectSubclass[class_exact:C] - CondBranch v16, bb5(), bb6() + CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() bb5(): - v19:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] + v18:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v31:BasicObject = GetIvar v19, :@foo - Jump bb4(v31) + v30:BasicObject = GetIvar v18, :@foo + Jump bb4(v30) bb6(): - v22:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v22) + v21:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v21) bb4(v15:BasicObject): CheckInterrupts Return v15 @@ -12805,10 +12919,10 @@ mod hir_opt_tests { v14:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v18:Fixnum = GuardType v10, Fixnum PatchPoint MethodRedefined(Integer@0x1010, to_s@0x1018, cme:0x1020) - v38:StringExact = CCallVariadic v18, :Integer#to_s@0x1048 - v29:StringExact = StringConcat v14, v38 + v37:StringExact = CCallVariadic v18, :Integer#to_s@0x1048 + v28:StringExact = StringConcat v14, v37 CheckInterrupts - Return v29 + Return v28 "); } @@ -15676,44 +15790,42 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): v19:ArrayExact = ToArray v13 - v22:CBool = HasType v12, ObjectSubclass[class_exact:CallerSplatA] - CondBranch v22, bb5(), bb6() + CondBranchHasType v12, ObjectSubclass[class_exact:CallerSplatA], bb5(), bb6() bb5(): - v25:ObjectSubclass[class_exact:CallerSplatA] = RefineType v12, ObjectSubclass[class_exact:CallerSplatA] + v24:ObjectSubclass[class_exact:CallerSplatA] = RefineType v12, ObjectSubclass[class_exact:CallerSplatA] PatchPoint NoSingletonClass(CallerSplatA@0x1008) - v42:CInt64 = ArrayLength v19 - v43:CInt64[1] = GuardBitEquals v42, CInt64(1) recompile - v44:CInt64 = CCall v19, :rb_jit_ruby2_keywords_splat_p@0x1010 - v45:CInt64[0] = GuardBitEquals v44, CInt64(0) + v40:CInt64 = ArrayLength v19 + v41:CInt64[1] = GuardBitEquals v40, CInt64(1) recompile + v42:CInt64 = CCall v19, :rb_jit_ruby2_keywords_splat_p@0x1010 + v43:CInt64[0] = GuardBitEquals v42, CInt64(0) PatchPoint MethodRedefined(CallerSplatA@0x1008, target@0x1011, cme:0x1018) - v47:CInt64[0] = Const CInt64(0) - v48:BasicObject = ArrayAref v19, v47 - v49:ArrayExact = NewArray v48 - PushInlineFrame :target, v25 (0x1040), num_args=1 + v45:CInt64[0] = Const CInt64(0) + v46:BasicObject = ArrayAref v19, v45 + v47:ArrayExact = NewArray v46 + PushInlineFrame :target, v24 (0x1040), num_args=1 CheckInterrupts PopInlineFrame - Jump bb4(v49) + Jump bb4(v47) bb6(): - v28:CBool = HasType v12, ObjectSubclass[class_exact:CallerSplatB] - CondBranch v28, bb7(), bb8() + CondBranchHasType v12, ObjectSubclass[class_exact:CallerSplatB], bb7(), bb8() bb7(): - v31:ObjectSubclass[class_exact:CallerSplatB] = RefineType v12, ObjectSubclass[class_exact:CallerSplatB] + v29:ObjectSubclass[class_exact:CallerSplatB] = RefineType v12, ObjectSubclass[class_exact:CallerSplatB] PatchPoint NoSingletonClass(CallerSplatB@0x1060) - v53:CInt64 = ArrayLength v19 - v54:CInt64[1] = GuardBitEquals v53, CInt64(1) recompile - v55:CInt64 = CCall v19, :rb_jit_ruby2_keywords_splat_p@0x1010 - v56:CInt64[0] = GuardBitEquals v55, CInt64(0) + v51:CInt64 = ArrayLength v19 + v52:CInt64[1] = GuardBitEquals v51, CInt64(1) recompile + v53:CInt64 = CCall v19, :rb_jit_ruby2_keywords_splat_p@0x1010 + v54:CInt64[0] = GuardBitEquals v53, CInt64(0) PatchPoint MethodRedefined(CallerSplatB@0x1060, target@0x1011, cme:0x1068) - v58:CInt64[0] = Const CInt64(0) - v59:BasicObject = ArrayAref v19, v58 - v60:ArrayExact = NewArray v59 - PushInlineFrame :target, v31 (0x1090), num_args=1 + v56:CInt64[0] = Const CInt64(0) + v57:BasicObject = ArrayAref v19, v56 + v58:ArrayExact = NewArray v57 + PushInlineFrame :target, v29 (0x1090), num_args=1 CheckInterrupts PopInlineFrame - Jump bb4(v60) + Jump bb4(v58) bb8(): - v34:BasicObject = Send v12, :target, v19 # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v34) + v32:BasicObject = Send v12, :target, v19 # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v32) bb4(v21:BasicObject): CheckInterrupts Return v21 @@ -17412,23 +17524,21 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint StableConstantNames(0x1008, String) v16:ClassSubclass[String@0x1010] = Const Value(VALUE(0x1010)) - v19:CBool = HasType v10, Fixnum - CondBranch v19, bb5(), bb6() + CondBranchHasType v10, Fixnum, bb5(), bb6() bb5(): PatchPoint MethodRedefined(Integer@0x1018, is_a?@0x1020, cme:0x1028) - v45:FalseClass = Const Value(false) - Jump bb4(v45) + v43:FalseClass = Const Value(false) + Jump bb4(v43) bb6(): - v25:CBool = HasType v10, StringExact - CondBranch v25, bb7(), bb8() + CondBranchHasType v10, StringExact, bb7(), bb8() bb7(): PatchPoint NoSingletonClass(String@0x1010) PatchPoint MethodRedefined(String@0x1010, is_a?@0x1020, cme:0x1028) - v46:TrueClass = Const Value(true) - Jump bb4(v46) + v44:TrueClass = Const Value(true) + Jump bb4(v44) bb8(): - v31:BasicObject = Send v10, :is_a?, v16 # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v31) + v29:BasicObject = Send v10, :is_a?, v16 # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v29) bb4(v18:BasicObject): CheckInterrupts Return v18 @@ -20303,31 +20413,29 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v16:CBool = HasType v10, ObjectSubclass[class_exact:C] - CondBranch v16, bb5(), bb6() + CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() bb5(): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v42:Fixnum[3] = Const Value(3) - Jump bb4(v42) + v40:Fixnum[3] = Const Value(3) + Jump bb4(v40) bb6(): - v22:CBool = HasType v10, ObjectSubclass[class_exact:D] - CondBranch v22, bb7(), bb8() + CondBranchHasType v10, ObjectSubclass[class_exact:D], bb7(), bb8() bb7(): PatchPoint NoSingletonClass(D@0x1040) PatchPoint MethodRedefined(D@0x1040, foo@0x1010, cme:0x1048) - v45:Fixnum[4] = Const Value(4) - Jump bb4(v45) + v43:Fixnum[4] = Const Value(4) + Jump bb4(v43) bb8(): - v28:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v28) + v26:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v26) bb4(v15:BasicObject): - v31:Fixnum[2] = Const Value(2) + v29:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1070, +@0x1078, cme:0x1080) - v48:Fixnum = GuardType v15, Fixnum recompile - v49:Fixnum = FixnumAdd v48, v31 + v46:Fixnum = GuardType v15, Fixnum recompile + v47:Fixnum = FixnumAdd v46, v29 CheckInterrupts - Return v49 + Return v47 "); } @@ -20357,23 +20465,21 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v16:CBool = HasType v10, ObjectSubclass[class_exact:C] - CondBranch v16, bb5(), bb6() + CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() bb5(): - v19:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] + v18:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, itself@0x1010, cme:0x1018) - Jump bb4(v19) + Jump bb4(v18) bb6(): - v22:CBool = HasType v10, Fixnum - CondBranch v22, bb7(), bb8() + CondBranchHasType v10, Fixnum, bb7(), bb8() bb7(): - v25:Fixnum = RefineType v10, Fixnum + v23:Fixnum = RefineType v10, Fixnum PatchPoint MethodRedefined(Integer@0x1040, itself@0x1010, cme:0x1018) - Jump bb4(v25) + Jump bb4(v23) bb8(): - v28:BasicObject = Send v10, :itself # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v28) + v26:BasicObject = Send v10, :itself # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v26) bb4(v15:BasicObject): CheckInterrupts Return v15 @@ -20413,11 +20519,11 @@ mod hir_opt_tests { PatchPoint StableConstantNames(0x1068, Integer) v31:ClassSubclass[Integer@0x1070] = Const Value(VALUE(0x1070)) PatchPoint MethodRedefined(Class@0x1078, ==@0x1080, cme:0x1088) - v82:CBool = IsBitEqual v12, v31 - v83:BoolExact = BoxBool v82 + v77:CBool = IsBitEqual v12, v31 + v78:BoolExact = BoxBool v77 PopInlineFrame CheckInterrupts - Return v83 + Return v78 "); } @@ -20453,33 +20559,31 @@ mod hir_opt_tests { v9:BasicObject = LoadArg :i@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - v21:CBool = HasType v12, ArrayExact - CondBranch v21, bb5(), bb6() + CondBranchHasType v12, ArrayExact, bb5(), bb6() bb5(): - v24:ArrayExact = RefineType v12, ArrayExact + v23:ArrayExact = RefineType v12, ArrayExact PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, []@0x1010, cme:0x1018) - v43:Fixnum = GuardType v13, Fixnum - v44:CInt64 = UnboxFixnum v43 - v45:CInt64 = ArrayLength v24 - v46:CInt64 = GuardLess v44, v45 - v47:CInt64 = AdjustBounds v46, v45 - v48:CInt64[0] = Const CInt64(0) - v49:CInt64 = GuardGreaterEq v47, v48 - v50:BasicObject = ArrayAref v24, v49 - Jump bb4(v50) + v41:Fixnum = GuardType v13, Fixnum + v42:CInt64 = UnboxFixnum v41 + v43:CInt64 = ArrayLength v23 + v44:CInt64 = GuardLess v42, v43 + v45:CInt64 = AdjustBounds v44, v43 + v46:CInt64[0] = Const CInt64(0) + v47:CInt64 = GuardGreaterEq v45, v46 + v48:BasicObject = ArrayAref v23, v47 + Jump bb4(v48) bb6(): - v27:CBool = HasType v12, HashExact - CondBranch v27, bb7(), bb8() + CondBranchHasType v12, HashExact, bb7(), bb8() bb7(): - v30:HashExact = RefineType v12, HashExact + v28:HashExact = RefineType v12, HashExact PatchPoint NoSingletonClass(Hash@0x1040) PatchPoint MethodRedefined(Hash@0x1040, []@0x1010, cme:0x1048) - v54:BasicObject = HashAref v30, v13 - Jump bb4(v54) + v52:BasicObject = HashAref v28, v13 + Jump bb4(v52) bb8(): - v33:BasicObject = Send v12, :[], v13 # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v33) + v31:BasicObject = Send v12, :[], v13 # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v31) bb4(v20:BasicObject): CheckInterrupts Return v20 @@ -20518,24 +20622,22 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v16:CBool = HasType v10, Fixnum - CondBranch v16, bb5(), bb6() + CondBranchHasType v10, Fixnum, bb5(), bb6() bb5(): - v19:Fixnum = RefineType v10, Fixnum + v18:Fixnum = RefineType v10, Fixnum PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018) - v37:StringExact = CCallVariadic v19, :Integer#to_s@0x1040 - Jump bb4(v37) + v35:StringExact = CCallVariadic v18, :Integer#to_s@0x1040 + Jump bb4(v35) bb6(): - v22:CBool = HasType v10, Bignum - CondBranch v22, bb7(), bb8() + CondBranchHasType v10, Bignum, bb7(), bb8() bb7(): - v25:Bignum = RefineType v10, Bignum + v23:Bignum = RefineType v10, Bignum PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018) - v40:StringExact = CCallVariadic v25, :Integer#to_s@0x1040 - Jump bb4(v40) + v38:StringExact = CCallVariadic v23, :Integer#to_s@0x1040 + Jump bb4(v38) bb8(): - v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v28) + v26:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v26) bb4(v15:BasicObject): CheckInterrupts Return v15 @@ -20571,24 +20673,22 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v16:CBool = HasType v10, Flonum - CondBranch v16, bb5(), bb6() + CondBranchHasType v10, Flonum, bb5(), bb6() bb5(): - v19:Flonum = RefineType v10, Flonum + v18:Flonum = RefineType v10, Flonum PatchPoint MethodRedefined(Float@0x1008, to_s@0x1010, cme:0x1018) - v37:BasicObject = CCallWithFrame v19, :Float#to_s@0x1040 - Jump bb4(v37) + v35:BasicObject = CCallWithFrame v18, :Float#to_s@0x1040 + Jump bb4(v35) bb6(): - v22:CBool = HasType v10, HeapFloat - CondBranch v22, bb7(), bb8() + CondBranchHasType v10, HeapFloat, bb7(), bb8() bb7(): - v25:HeapFloat = RefineType v10, HeapFloat + v23:HeapFloat = RefineType v10, HeapFloat PatchPoint MethodRedefined(Float@0x1008, to_s@0x1010, cme:0x1018) - v40:BasicObject = CCallWithFrame v25, :Float#to_s@0x1040 - Jump bb4(v40) + v38:BasicObject = CCallWithFrame v23, :Float#to_s@0x1040 + Jump bb4(v38) bb8(): - v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v28) + v26:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v26) bb4(v15:BasicObject): CheckInterrupts Return v15 @@ -20624,24 +20724,22 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v16:CBool = HasType v10, StaticSymbol - CondBranch v16, bb5(), bb6() + CondBranchHasType v10, StaticSymbol, bb5(), bb6() bb5(): - v19:StaticSymbol = RefineType v10, StaticSymbol + v18:StaticSymbol = RefineType v10, StaticSymbol PatchPoint MethodRedefined(Symbol@0x1008, to_s@0x1010, cme:0x1018) - v36:StringExact = InvokeBuiltin leaf , v19 - Jump bb4(v36) + v34:StringExact = InvokeBuiltin leaf , v18 + Jump bb4(v34) bb6(): - v22:CBool = HasType v10, DynamicSymbol - CondBranch v22, bb7(), bb8() + CondBranchHasType v10, DynamicSymbol, bb7(), bb8() bb7(): - v25:DynamicSymbol = RefineType v10, DynamicSymbol + v23:DynamicSymbol = RefineType v10, DynamicSymbol PatchPoint MethodRedefined(Symbol@0x1008, to_s@0x1010, cme:0x1018) - v38:StringExact = InvokeBuiltin leaf , v25 - Jump bb4(v38) + v36:StringExact = InvokeBuiltin leaf , v23 + Jump bb4(v36) bb8(): - v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v28) + v26:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v26) bb4(v15:BasicObject): CheckInterrupts Return v15 @@ -20681,16 +20779,15 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v16:CBool = HasType v10, ObjectSubclass[class_exact:C] - CondBranch v16, bb5(), bb6() + CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() bb5(): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v31:Fixnum[3] = Const Value(3) - Jump bb4(v31) + v30:Fixnum[3] = Const Value(3) + Jump bb4(v30) bb6(): - v22:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v22) + v21:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v21) bb4(v15:BasicObject): CheckInterrupts Return v15 @@ -22198,25 +22295,23 @@ mod hir_opt_tests { v9:BasicObject = LoadArg :b@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - v21:CBool = HasType v12, HeapFloat - CondBranch v21, bb5(), bb6() + CondBranchHasType v12, HeapFloat, bb5(), bb6() bb5(): - v24:HeapFloat = RefineType v12, HeapFloat + v23:HeapFloat = RefineType v12, HeapFloat PatchPoint MethodRedefined(Float@0x1008, *@0x1010, cme:0x1018) - v42:BasicObject = CCallWithFrame v24, :Float#*@0x1040, v13 - Jump bb4(v42) + v40:BasicObject = CCallWithFrame v23, :Float#*@0x1040, v13 + Jump bb4(v40) bb6(): - v27:CBool = HasType v12, Flonum - CondBranch v27, bb7(), bb8() + CondBranchHasType v12, Flonum, bb7(), bb8() bb7(): - v30:Flonum = RefineType v12, Flonum + v28:Flonum = RefineType v12, Flonum PatchPoint MethodRedefined(Float@0x1008, *@0x1010, cme:0x1018) - v45:Flonum = GuardType v13, Flonum recompile - v46:Float = FloatMul v30, v45 - Jump bb4(v46) + v43:Flonum = GuardType v13, Flonum recompile + v44:Float = FloatMul v28, v43 + Jump bb4(v44) bb8(): - v33:BasicObject = Send v12, :*, v13 # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v33) + v31:BasicObject = Send v12, :*, v13 # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v31) bb4(v20:BasicObject): CheckInterrupts Return v20 @@ -22490,7 +22585,7 @@ mod hir_opt_tests { v8:BasicObject = LoadArg :x@1 Jump bb3(v7, v8) bb3(v11:HeapBasicObject, v12:BasicObject): - v92:NilClass = Const Value(nil) + v90:NilClass = Const Value(nil) v17:Fixnum[1] = Const Value(1) v20:CShape = LoadField v11, :shape_id@0x1001 v21:CShape[0x1002] = Const CShape(0x1002) @@ -22514,31 +22609,29 @@ mod hir_opt_tests { bb4(): PatchPoint NoEPEscape(f) v43:Fixnum[1] = Const Value(1) - v47:CBool = HasType v12, Fixnum - CondBranch v47, bb10(), bb11() + CondBranchHasType v12, Fixnum, bb10(), bb11() bb10(): - v50:Fixnum = RefineType v12, Fixnum + v49:Fixnum = RefineType v12, Fixnum PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v84:Fixnum = FixnumAdd v50, v43 - Jump bb9(v84) + v82:Fixnum = FixnumAdd v49, v43 + Jump bb9(v82) bb11(): - v53:CBool = HasType v12, Flonum - CondBranch v53, bb12(), bb13() + CondBranchHasType v12, Flonum, bb12(), bb13() bb12(): - v56:Flonum = RefineType v12, Flonum + v54:Flonum = RefineType v12, Flonum PatchPoint MethodRedefined(Float@0x1040, +@0x1010, cme:0x1048) - v87:Float = FloatAdd v56, v43 - Jump bb9(v87) + v85:Float = FloatAdd v54, v43 + Jump bb9(v85) bb13(): PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v90:Fixnum = GuardType v12, Fixnum recompile - v91:Fixnum = FixnumAdd v90, v43 - Jump bb9(v91) + v88:Fixnum = GuardType v12, Fixnum recompile + v89:Fixnum = FixnumAdd v88, v43 + Jump bb9(v89) bb9(v46:Float|Fixnum): - v67:CShape = LoadField v11, :shape_id@0x1001 - v68:CShape[0x1002] = Const CShape(0x1002) - v69:CBool = IsBitEqual v67, v68 - CondBranch v69, bb15(), bb16() + v65:CShape = LoadField v11, :shape_id@0x1001 + v66:CShape[0x1002] = Const CShape(0x1002) + v67:CBool = IsBitEqual v65, v66 + CondBranch v67, bb15(), bb16() bb15(): StoreField v11, :@a@0x1003, v46 WriteBarrier v11, v46 @@ -24457,56 +24550,53 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :obj@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v16:CBool = HasType v10, ObjectSubclass[class_exact:C] - CondBranch v16, bb5(), bb6() + CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() bb5(): - v19:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] + v18:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - PushInlineFrame :foo, v19 (0x1040), num_args=0 - v57:CPtr = GetEP 0 - v58:CInt64 = LoadField v57, :VM_ENV_DATA_INDEX_SPECVAL@0x1060 - v59:CInt64[-4] = Const CInt64(-4) - v60:CInt64 = IntAnd v58, v59 - v61:BasicObject = InvokeBlockIseqDirect (0x1068), v60 + PushInlineFrame :foo, v18 (0x1040), num_args=0 + v54:CPtr = GetEP 0 + v55:CInt64 = LoadField v54, :VM_ENV_DATA_INDEX_SPECVAL@0x1060 + v56:CInt64[-4] = Const CInt64(-4) + v57:CInt64 = IntAnd v55, v56 + v58:BasicObject = InvokeBlockIseqDirect (0x1068), v57 CheckInterrupts PopInlineFrame - Jump bb4(v61) + Jump bb4(v58) bb6(): - v22:CBool = HasType v10, ObjectSubclass[class_exact:A] - CondBranch v22, bb7(), bb8() + CondBranchHasType v10, ObjectSubclass[class_exact:A], bb7(), bb8() bb7(): - v25:ObjectSubclass[class_exact:A] = RefineType v10, ObjectSubclass[class_exact:A] + v23:ObjectSubclass[class_exact:A] = RefineType v10, ObjectSubclass[class_exact:A] PatchPoint NoSingletonClass(A@0x1088) PatchPoint MethodRedefined(A@0x1088, foo@0x1010, cme:0x1018) - PushInlineFrame :foo, v25 (0x1040), num_args=0 - v75:CPtr = GetEP 0 - v76:CInt64 = LoadField v75, :VM_ENV_DATA_INDEX_SPECVAL@0x1060 - v77:CInt64[-4] = Const CInt64(-4) - v78:CInt64 = IntAnd v76, v77 - v79:BasicObject = InvokeBlockIseqDirect (0x1068), v78 + PushInlineFrame :foo, v23 (0x1040), num_args=0 + v72:CPtr = GetEP 0 + v73:CInt64 = LoadField v72, :VM_ENV_DATA_INDEX_SPECVAL@0x1060 + v74:CInt64[-4] = Const CInt64(-4) + v75:CInt64 = IntAnd v73, v74 + v76:BasicObject = InvokeBlockIseqDirect (0x1068), v75 CheckInterrupts PopInlineFrame - Jump bb4(v79) + Jump bb4(v76) bb8(): - v28:CBool = HasType v10, ObjectSubclass[class_exact:B] - CondBranch v28, bb9(), bb10() + CondBranchHasType v10, ObjectSubclass[class_exact:B], bb9(), bb10() bb9(): - v31:ObjectSubclass[class_exact:B] = RefineType v10, ObjectSubclass[class_exact:B] + v28:ObjectSubclass[class_exact:B] = RefineType v10, ObjectSubclass[class_exact:B] PatchPoint NoSingletonClass(B@0x1090) PatchPoint MethodRedefined(B@0x1090, foo@0x1010, cme:0x1018) - PushInlineFrame :foo, v31 (0x1040), num_args=0 - v93:CPtr = GetEP 0 - v94:CInt64 = LoadField v93, :VM_ENV_DATA_INDEX_SPECVAL@0x1060 - v95:CInt64[-4] = Const CInt64(-4) - v96:CInt64 = IntAnd v94, v95 - v97:BasicObject = InvokeBlockIseqDirect (0x1068), v96 + PushInlineFrame :foo, v28 (0x1040), num_args=0 + v90:CPtr = GetEP 0 + v91:CInt64 = LoadField v90, :VM_ENV_DATA_INDEX_SPECVAL@0x1060 + v92:CInt64[-4] = Const CInt64(-4) + v93:CInt64 = IntAnd v91, v92 + v94:BasicObject = InvokeBlockIseqDirect (0x1068), v93 CheckInterrupts PopInlineFrame - Jump bb4(v97) + Jump bb4(v94) bb10(): - v34:BasicObject = Send v10, 0x1068, :foo # SendFallbackReason: Send: polymorphic fallback - Jump bb4(v34) + v31:BasicObject = Send v10, 0x1068, :foo # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v31) bb4(v15:BasicObject): PatchPoint NoEPEscape(test) CheckInterrupts @@ -24589,26 +24679,24 @@ mod hir_opt_tests { v51:BasicObject = LoadField v20, :VM_ENV_DATA_INDEX_SPECVAL@0x1004 Jump bb6(v51, v13) bb6(v18:BasicObject, v19:BasicObject): - v55:CBool = HasType v12, ObjectSubclass[class_exact:B] - CondBranch v55, bb15(), bb16() + CondBranchHasType v12, ObjectSubclass[class_exact:B], bb15(), bb16() bb15(): - v74:NilClass = GuardBitEquals v18, Value(nil) recompile + v72:NilClass = GuardBitEquals v18, Value(nil) recompile PatchPoint NoSingletonClass(B@0x1010) PatchPoint MethodRedefined(B@0x1010, foo@0x1018, cme:0x1020) - v78:Fixnum[43] = Const Value(43) - Jump bb14(v78) + v76:Fixnum[43] = Const Value(43) + Jump bb14(v76) bb16(): - v61:CBool = HasType v12, ObjectSubclass[class_exact:A] - CondBranch v61, bb17(), bb18() + CondBranchHasType v12, ObjectSubclass[class_exact:A], bb17(), bb18() bb17(): - v79:NilClass = GuardBitEquals v18, Value(nil) recompile + v77:NilClass = GuardBitEquals v18, Value(nil) recompile PatchPoint NoSingletonClass(A@0x1048) PatchPoint MethodRedefined(A@0x1048, foo@0x1018, cme:0x1050) - v83:Fixnum[42] = Const Value(42) - Jump bb14(v83) + v81:Fixnum[42] = Const Value(42) + Jump bb14(v81) bb18(): - v67:BasicObject = Send v12, &block, :foo, v18 # SendFallbackReason: Send: polymorphic fallback - Jump bb14(v67) + v65:BasicObject = Send v12, &block, :foo, v18 # SendFallbackReason: Send: polymorphic fallback + Jump bb14(v65) bb14(v54:BasicObject): CheckInterrupts Return v54 diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 2894c25f5f299c..964209a3f1d981 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2409,29 +2409,27 @@ pub(crate) mod hir_build_tests { bb3(v6:BasicObject): v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:Fixnum[123] = Const Value(123) - v15:CBool[false] = HasType v12, String - CondBranch v15, bb4(), bb5() + CondBranchHasType v12, String, bb4(), bb5() bb4(): - v17 = RefineType v12, String - Jump bb6(v17) + v16 = RefineType v12, String + Jump bb6(v16) bb5(): - v19:Fixnum[123] = RefineType v12, NotString - v20:BasicObject = Send v19, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb6(v20) - bb6(v22:BasicObject): - v24:CBool = HasType v22, String - CondBranch v24, bb7(), bb8() + v18:Fixnum[123] = RefineType v12, NotString + v19:BasicObject = Send v18, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v19) + bb6(v21:BasicObject): + CondBranchHasType v21, String, bb7(), bb8() bb7(): - v26:String = RefineType v22, String - Jump bb9(v26) + v24:String = RefineType v21, String + Jump bb9(v24) bb8(): - v28:StringExact = AnyToString v12 - Jump bb9(v28) - bb9(v30:String): - v32:StringExact = StringConcat v10, v30 - v34:Symbol = StringIntern v32 + v26:StringExact = AnyToString v12 + Jump bb9(v26) + bb9(v28:String): + v30:StringExact = StringConcat v10, v28 + v32:Symbol = StringIntern v30 CheckInterrupts - Return v34 + Return v32 "); } @@ -5318,16 +5316,15 @@ pub(crate) mod hir_build_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - v16:CBool = HasType v10, NilClass - v17:NilClass = Const Value(nil) - CondBranch v16, bb4(v9, v17, v17), bb5() + v16:NilClass = Const Value(nil) + CondBranchHasType v10, NilClass, bb4(v9, v16, v16), bb5() bb5(): - v19:NotNil = RefineType v10, NotNil - v21:BasicObject = Send v19, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) - Jump bb4(v9, v19, v21) - bb4(v23:BasicObject, v24:BasicObject, v25:BasicObject): + v18:NotNil = RefineType v10, NotNil + v20:BasicObject = Send v18, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) + Jump bb4(v9, v18, v20) + bb4(v22:BasicObject, v23:BasicObject, v24:BasicObject): CheckInterrupts - Return v25 + Return v24 "); } @@ -5363,20 +5360,19 @@ pub(crate) mod hir_build_tests { CondBranch v15, bb6(), bb4(v9, v16) bb6(): v18:Truthy = RefineType v10, Truthy - v23:CBool[false] = HasType v18, NilClass - v24:NilClass = Const Value(nil) - CondBranch v23, bb5(v9, v24, v24), bb7() + v23:NilClass = Const Value(nil) + CondBranchHasType v18, NilClass, bb5(v9, v23, v23), bb7() bb7(): - v26:Truthy = RefineType v18, NotNil - v28:BasicObject = Send v26, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) + v25:Truthy = RefineType v18, NotNil + v27:BasicObject = Send v25, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) CheckInterrupts - Return v28 - bb4(v33:BasicObject, v34:Falsy): - v38:Fixnum[4] = Const Value(4) - Jump bb5(v33, v34, v38) - bb5(v40:BasicObject, v41:Falsy, v42:Fixnum[4]): + Return v27 + bb4(v32:BasicObject, v33:Falsy): + v37:Fixnum[4] = Const Value(4) + Jump bb5(v32, v33, v37) + bb5(v39:BasicObject, v40:Falsy, v41:Fixnum[4]): CheckInterrupts - Return v42 + Return v41 "); } @@ -5797,28 +5793,26 @@ pub(crate) mod hir_build_tests { bb3(v6:BasicObject): v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:Fixnum[1] = Const Value(1) - v15:CBool[false] = HasType v12, String - CondBranch v15, bb4(), bb5() + CondBranchHasType v12, String, bb4(), bb5() bb4(): - v17 = RefineType v12, String - Jump bb6(v17) + v16 = RefineType v12, String + Jump bb6(v16) bb5(): - v19:Fixnum[1] = RefineType v12, NotString - v20:BasicObject = Send v19, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb6(v20) - bb6(v22:BasicObject): - v24:CBool = HasType v22, String - CondBranch v24, bb7(), bb8() + v18:Fixnum[1] = RefineType v12, NotString + v19:BasicObject = Send v18, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v19) + bb6(v21:BasicObject): + CondBranchHasType v21, String, bb7(), bb8() bb7(): - v26:String = RefineType v22, String - Jump bb9(v26) + v24:String = RefineType v21, String + Jump bb9(v24) bb8(): - v28:StringExact = AnyToString v12 - Jump bb9(v28) - bb9(v30:String): - v32:StringExact = StringConcat v10, v30 + v26:StringExact = AnyToString v12 + Jump bb9(v26) + bb9(v28:String): + v30:StringExact = StringConcat v10, v28 CheckInterrupts - Return v32 + Return v30 "); } @@ -5840,68 +5834,62 @@ pub(crate) mod hir_build_tests { Jump bb3(v4) bb3(v6:BasicObject): v10:Fixnum[1] = Const Value(1) - v13:CBool[false] = HasType v10, String - CondBranch v13, bb4(), bb5() + CondBranchHasType v10, String, bb4(), bb5() bb4(): - v15 = RefineType v10, String - Jump bb6(v15) + v14 = RefineType v10, String + Jump bb6(v14) bb5(): - v17:Fixnum[1] = RefineType v10, NotString - v18:BasicObject = Send v17, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb6(v18) - bb6(v20:BasicObject): - v22:CBool = HasType v20, String - CondBranch v22, bb7(), bb8() + v16:Fixnum[1] = RefineType v10, NotString + v17:BasicObject = Send v16, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v17) + bb6(v19:BasicObject): + CondBranchHasType v19, String, bb7(), bb8() bb7(): - v24:String = RefineType v20, String - Jump bb9(v24) + v22:String = RefineType v19, String + Jump bb9(v22) bb8(): - v26:StringExact = AnyToString v10 - Jump bb9(v26) - bb9(v28:String): - v30:Fixnum[2] = Const Value(2) - v33:CBool[false] = HasType v30, String - CondBranch v33, bb10(), bb11() + v24:StringExact = AnyToString v10 + Jump bb9(v24) + bb9(v26:String): + v28:Fixnum[2] = Const Value(2) + CondBranchHasType v28, String, bb10(), bb11() bb10(): - v35 = RefineType v30, String - Jump bb12(v35) + v32 = RefineType v28, String + Jump bb12(v32) bb11(): - v37:Fixnum[2] = RefineType v30, NotString - v38:BasicObject = Send v37, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb12(v38) - bb12(v40:BasicObject): - v42:CBool = HasType v40, String - CondBranch v42, bb13(), bb14() + v34:Fixnum[2] = RefineType v28, NotString + v35:BasicObject = Send v34, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb12(v35) + bb12(v37:BasicObject): + CondBranchHasType v37, String, bb13(), bb14() bb13(): - v44:String = RefineType v40, String - Jump bb15(v44) + v40:String = RefineType v37, String + Jump bb15(v40) bb14(): - v46:StringExact = AnyToString v30 - Jump bb15(v46) - bb15(v48:String): - v50:Fixnum[3] = Const Value(3) - v53:CBool[false] = HasType v50, String - CondBranch v53, bb16(), bb17() + v42:StringExact = AnyToString v28 + Jump bb15(v42) + bb15(v44:String): + v46:Fixnum[3] = Const Value(3) + CondBranchHasType v46, String, bb16(), bb17() bb16(): - v55 = RefineType v50, String - Jump bb18(v55) + v50 = RefineType v46, String + Jump bb18(v50) bb17(): - v57:Fixnum[3] = RefineType v50, NotString - v58:BasicObject = Send v57, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb18(v58) - bb18(v60:BasicObject): - v62:CBool = HasType v60, String - CondBranch v62, bb19(), bb20() + v52:Fixnum[3] = RefineType v46, NotString + v53:BasicObject = Send v52, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb18(v53) + bb18(v55:BasicObject): + CondBranchHasType v55, String, bb19(), bb20() bb19(): - v64:String = RefineType v60, String - Jump bb21(v64) + v58:String = RefineType v55, String + Jump bb21(v58) bb20(): - v66:StringExact = AnyToString v50 - Jump bb21(v66) - bb21(v68:String): - v70:StringExact = StringConcat v28, v48, v68 + v60:StringExact = AnyToString v46 + Jump bb21(v60) + bb21(v62:String): + v64:StringExact = StringConcat v26, v44, v62 CheckInterrupts - Return v70 + Return v64 "); } @@ -5924,28 +5912,26 @@ pub(crate) mod hir_build_tests { bb3(v6:BasicObject): v10:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:NilClass = Const Value(nil) - v15:CBool[false] = HasType v12, String - CondBranch v15, bb4(), bb5() + CondBranchHasType v12, String, bb4(), bb5() bb4(): - v17 = RefineType v12, String - Jump bb6(v17) + v16 = RefineType v12, String + Jump bb6(v16) bb5(): - v19:NilClass = RefineType v12, NotString - v20:BasicObject = Send v19, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb6(v20) - bb6(v22:BasicObject): - v24:CBool = HasType v22, String - CondBranch v24, bb7(), bb8() + v18:NilClass = RefineType v12, NotString + v19:BasicObject = Send v18, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v19) + bb6(v21:BasicObject): + CondBranchHasType v21, String, bb7(), bb8() bb7(): - v26:String = RefineType v22, String - Jump bb9(v26) + v24:String = RefineType v21, String + Jump bb9(v24) bb8(): - v28:StringExact = AnyToString v12 - Jump bb9(v28) - bb9(v30:String): - v32:StringExact = StringConcat v10, v30 + v26:StringExact = AnyToString v12 + Jump bb9(v26) + bb9(v28:String): + v30:StringExact = StringConcat v10, v28 CheckInterrupts - Return v32 + Return v30 "); } @@ -5967,68 +5953,62 @@ pub(crate) mod hir_build_tests { Jump bb3(v4) bb3(v6:BasicObject): v10:Fixnum[1] = Const Value(1) - v13:CBool[false] = HasType v10, String - CondBranch v13, bb4(), bb5() + CondBranchHasType v10, String, bb4(), bb5() bb4(): - v15 = RefineType v10, String - Jump bb6(v15) + v14 = RefineType v10, String + Jump bb6(v14) bb5(): - v17:Fixnum[1] = RefineType v10, NotString - v18:BasicObject = Send v17, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb6(v18) - bb6(v20:BasicObject): - v22:CBool = HasType v20, String - CondBranch v22, bb7(), bb8() + v16:Fixnum[1] = RefineType v10, NotString + v17:BasicObject = Send v16, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v17) + bb6(v19:BasicObject): + CondBranchHasType v19, String, bb7(), bb8() bb7(): - v24:String = RefineType v20, String - Jump bb9(v24) + v22:String = RefineType v19, String + Jump bb9(v22) bb8(): - v26:StringExact = AnyToString v10 - Jump bb9(v26) - bb9(v28:String): - v30:Fixnum[2] = Const Value(2) - v33:CBool[false] = HasType v30, String - CondBranch v33, bb10(), bb11() + v24:StringExact = AnyToString v10 + Jump bb9(v24) + bb9(v26:String): + v28:Fixnum[2] = Const Value(2) + CondBranchHasType v28, String, bb10(), bb11() bb10(): - v35 = RefineType v30, String - Jump bb12(v35) + v32 = RefineType v28, String + Jump bb12(v32) bb11(): - v37:Fixnum[2] = RefineType v30, NotString - v38:BasicObject = Send v37, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb12(v38) - bb12(v40:BasicObject): - v42:CBool = HasType v40, String - CondBranch v42, bb13(), bb14() + v34:Fixnum[2] = RefineType v28, NotString + v35:BasicObject = Send v34, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb12(v35) + bb12(v37:BasicObject): + CondBranchHasType v37, String, bb13(), bb14() bb13(): - v44:String = RefineType v40, String - Jump bb15(v44) + v40:String = RefineType v37, String + Jump bb15(v40) bb14(): - v46:StringExact = AnyToString v30 - Jump bb15(v46) - bb15(v48:String): - v50:Fixnum[3] = Const Value(3) - v53:CBool[false] = HasType v50, String - CondBranch v53, bb16(), bb17() + v42:StringExact = AnyToString v28 + Jump bb15(v42) + bb15(v44:String): + v46:Fixnum[3] = Const Value(3) + CondBranchHasType v46, String, bb16(), bb17() bb16(): - v55 = RefineType v50, String - Jump bb18(v55) + v50 = RefineType v46, String + Jump bb18(v50) bb17(): - v57:Fixnum[3] = RefineType v50, NotString - v58:BasicObject = Send v57, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb18(v58) - bb18(v60:BasicObject): - v62:CBool = HasType v60, String - CondBranch v62, bb19(), bb20() + v52:Fixnum[3] = RefineType v46, NotString + v53:BasicObject = Send v52, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb18(v53) + bb18(v55:BasicObject): + CondBranchHasType v55, String, bb19(), bb20() bb19(): - v64:String = RefineType v60, String - Jump bb21(v64) + v58:String = RefineType v55, String + Jump bb21(v58) bb20(): - v66:StringExact = AnyToString v50 - Jump bb21(v66) - bb21(v68:String): - v70:RegexpExact = ToRegexp v28, v48, v68 + v60:StringExact = AnyToString v46 + Jump bb21(v60) + bb21(v62:String): + v64:RegexpExact = ToRegexp v26, v44, v62 CheckInterrupts - Return v70 + Return v64 "); } @@ -6050,48 +6030,44 @@ pub(crate) mod hir_build_tests { Jump bb3(v4) bb3(v6:BasicObject): v10:Fixnum[1] = Const Value(1) - v13:CBool[false] = HasType v10, String - CondBranch v13, bb4(), bb5() + CondBranchHasType v10, String, bb4(), bb5() bb4(): - v15 = RefineType v10, String - Jump bb6(v15) + v14 = RefineType v10, String + Jump bb6(v14) bb5(): - v17:Fixnum[1] = RefineType v10, NotString - v18:BasicObject = Send v17, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb6(v18) - bb6(v20:BasicObject): - v22:CBool = HasType v20, String - CondBranch v22, bb7(), bb8() + v16:Fixnum[1] = RefineType v10, NotString + v17:BasicObject = Send v16, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb6(v17) + bb6(v19:BasicObject): + CondBranchHasType v19, String, bb7(), bb8() bb7(): - v24:String = RefineType v20, String - Jump bb9(v24) + v22:String = RefineType v19, String + Jump bb9(v22) bb8(): - v26:StringExact = AnyToString v10 - Jump bb9(v26) - bb9(v28:String): - v30:Fixnum[2] = Const Value(2) - v33:CBool[false] = HasType v30, String - CondBranch v33, bb10(), bb11() + v24:StringExact = AnyToString v10 + Jump bb9(v24) + bb9(v26:String): + v28:Fixnum[2] = Const Value(2) + CondBranchHasType v28, String, bb10(), bb11() bb10(): - v35 = RefineType v30, String - Jump bb12(v35) + v32 = RefineType v28, String + Jump bb12(v32) bb11(): - v37:Fixnum[2] = RefineType v30, NotString - v38:BasicObject = Send v37, :to_s # SendFallbackReason: ObjToString: result is not a string - Jump bb12(v38) - bb12(v40:BasicObject): - v42:CBool = HasType v40, String - CondBranch v42, bb13(), bb14() + v34:Fixnum[2] = RefineType v28, NotString + v35:BasicObject = Send v34, :to_s # SendFallbackReason: ObjToString: result is not a string + Jump bb12(v35) + bb12(v37:BasicObject): + CondBranchHasType v37, String, bb13(), bb14() bb13(): - v44:String = RefineType v40, String - Jump bb15(v44) + v40:String = RefineType v37, String + Jump bb15(v40) bb14(): - v46:StringExact = AnyToString v30 - Jump bb15(v46) - bb15(v48:String): - v50:RegexpExact = ToRegexp v28, v48, MULTILINE|IGNORECASE|EXTENDED|NOENCODING + v42:StringExact = AnyToString v28 + Jump bb15(v42) + bb15(v44:String): + v46:RegexpExact = ToRegexp v26, v44, MULTILINE|IGNORECASE|EXTENDED|NOENCODING CheckInterrupts - Return v50 + Return v46 "); } diff --git a/zjit/src/options.rs b/zjit/src/options.rs index a4b3d4cec3ea76..363aaea7c7e30e 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -711,6 +711,14 @@ pub fn set_inline_threshold(inline_threshold: InlineThreshold) { unsafe { OPTIONS.as_mut().unwrap().inline_threshold = inline_threshold; } } +/// Update --zjit-num-profiles for testing +#[cfg(test)] +pub fn set_num_profiles(num_profiles: NumProfiles) { + rb_zjit_prepare_options(); + unsafe { OPTIONS.as_mut().unwrap().num_profiles = num_profiles; } + update_profile_threshold(); +} + /// Set --zjit-mem-size for testing. It's used to force OOM in tests. #[cfg(test)] pub fn set_mem_bytes(mem_bytes: usize) { From f016ece95fe8f2a56842740c806b3551ab2e15f7 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 22 Sep 2026 19:01:50 -0500 Subject: [PATCH 28/31] [DOC] Harmonize setgid? methods (#18963) --- file.c | 48 +++++++++++++++++++++++++++++++++++++-------- pathname_builtin.rb | 9 ++++----- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/file.c b/file.c index e12f26e2a918f7..b94e76e1479e42 100644 --- a/file.c +++ b/file.c @@ -2512,12 +2512,28 @@ rb_file_suid_p(VALUE obj, VALUE fname) } /* + * :markup: markdown + * * call-seq: - * File.setgid?(file_name) -> true or false + * File.setgid?(object) -> true or false * - * Returns +true+ if the named file has the setgid bit set. + * Returns whether the setgid bit is set + * in the [special bits](rdoc-ref:file/filesystem_modes.md@Special+Bits) + * for the given `object`, which may be a path or an IO object: * - * _file_name_ can be an IO object. + * ```ruby + * path = '/tmp/t.tmp' + * File.write(path, 'foo') + * mode = File.stat(path).mode.to_s(8) # => "100664" + * File.setgid?(path) # => false + * File.chmod(0o2644, path) # Set the bit. + * mode = File.stat(path).mode.to_s(8) # => "102644" + * File.setgid?(path) # => true + * File.delete(path) # Clean up. + * File.setgid?($stdin) # => false + * ``` + * + * On Windows, the bit is never set; the method always returns `false`. */ static VALUE @@ -7393,15 +7409,31 @@ rb_stat_suid(VALUE obj) } /* + * :markup: markdown + * * call-seq: - * stat.setgid? -> true or false + * setgid? -> true or false * - * Returns +true+ if stat has the set-group-id permission bit set, - * +false+ if it doesn't or if the operating system doesn't support this - * feature. + * Returns whether the setgid bit is set + * in the [special bits](rdoc-ref:file/filesystem_modes.md@Special+Bits) + * for the entry represented in `self`: * - * File.stat("/usr/sbin/lpc").setgid? #=> true + * ```ruby + * path = '/tmp/t.tmp' + * File.write(path, 'foo') + * stat = File.stat(path) # Take a snapshot. + * stat.setgid? # => false + * stat.mode.to_s(8) # => "100664" + * File.chmod(0o2644, path) # Set the bit; stat snapshot unchanged. + * stat.setgid? # => false + * stat.mode.to_s(8) # => "100664" + * stat = File.stat(path) # Fresh stat; snapshot changed. + * stat.setgid? # => true + * stat.mode.to_s(8) # => "102644" + * File.delete(path) # Clean up. + * ``` * + * On Windows, the bit is never set; the method always returns `false`. */ static VALUE diff --git a/pathname_builtin.rb b/pathname_builtin.rb index ad1700b8dc5cd1..76d4bc7a967f0b 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2620,17 +2620,16 @@ def setuid?() FileTest.setuid?(@path) end # call-seq: # setgid? -> true or false # - # Returns whether the [setgid bit](https://en.wikipedia.org/wiki/Setuid) is set - # in the permissions for the entry at the path in `self`: + # Returns whether the setgid bit is set + # in the [special bits](rdoc-ref:file/filesystem_modes.md@Special+Bits) + # for the entry at the path in `self`: # # ```ruby - # # Create a file and get its permissions and setgid? setting. # pn = Pathname('doc/t.tmp') # pn.write('foo') # mode = pn.stat.mode.to_s(8) # => "100664" # pn.setgid? # => false - # # Set the bit. - # pn.chmod(0o2644) + # pn.chmod(0o2644) # Set the bit. # mode = pn.stat.mode.to_s(8) # => "102644" # pn.setgid? # => true # pn.delete # Clean up. From ffc14da798a598fc07cf4175f6bc15a79cd6be9b Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 22 Sep 2026 19:02:39 -0500 Subject: [PATCH 29/31] [DOC] Harmonize setuid? methods (#18965) --- file.c | 49 +++++++++++++++++++++++++++++++++++++-------- pathname_builtin.rb | 9 ++++----- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/file.c b/file.c index b94e76e1479e42..294edcd47040c5 100644 --- a/file.c +++ b/file.c @@ -2493,12 +2493,28 @@ check3rdbyte(VALUE fname, int mode) #endif /* + * :markup: markdown + * * call-seq: - * File.setuid?(file_name) -> true or false + * File.setuid?(object) -> true or false * - * Returns +true+ if the named file has the setuid bit set. + * Returns whether the setuid bit is set + * in the [special bits](rdoc-ref:file/filesystem_modes.md@Special+Bits) + * for the given `object`, which may be a path or an IO object: * - * _file_name_ can be an IO object. + * ```ruby + * path = '/tmp/t.tmp' + * File.write(path, 'foo') + * mode = File.stat(path).mode.to_s(8) # => "100664" + * File.setuid?(path) # => false + * File.chmod(0o4644, path) # Set the bit. + * mode = File.stat(path).mode.to_s(8) # => "104644" + * File.setuid?(path) # => true + * File.delete(path) # Clean up. + * File.setuid?($stdin) # => false + * ``` + * + * On Windows, the bit is never set; the method always returns `false`. */ static VALUE @@ -7389,14 +7405,31 @@ rb_stat_s(VALUE obj) } /* + * :markup: markdown + * * call-seq: - * stat.setuid? -> true or false + * setuid? -> true or false * - * Returns +true+ if stat has the set-user-id permission bit set, - * +false+ if it doesn't or if the operating system doesn't support this - * feature. + * Returns whether the setuid bit is set + * in the [special bits](rdoc-ref:file/filesystem_modes.md@Special+Bits) + * for the entry represented in `self`: * - * File.stat("/bin/su").setuid? #=> true + * ```ruby + * path = '/tmp/t.tmp' + * File.write(path, 'foo') + * stat = File.stat(path) # Take snapshot; bit not set. + * stat.setuid? # => false + * stat.mode.to_s(8) # => "100664" + * File.chmod(0o4644, path) # Set the bit; snapshot not updated. + * stat.setuid? # => false + * stat.mode.to_s(8) # => "100664" + * stat = File.stat(path) # Fresh snapshot. + * stat.setuid? # => true + * stat.mode.to_s(8) # => "104644" + * File.delete(path) # Clean up. + * ``` + * + * On Windows, the bit is never set; the method always returns `false`. */ static VALUE diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 76d4bc7a967f0b..0fff9c508e2995 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -2596,17 +2596,16 @@ def readable_real?() FileTest.readable_real?(@path) end # call-seq: # setuid? -> true or false # - # Returns whether the [setuid bit](https://en.wikipedia.org/wiki/Setuid) is set - # in the permissions for the entry at the path in `self`: + # Returns whether the setuid bit is set + # in the [special bits](rdoc-ref:file/filesystem_modes.md@Special+Bits) + # for the entry at the path in `self`: # # ```ruby - # # Create a file and get its permissions and setuid? setting. # pn = Pathname('doc/t.tmp') # pn.write('foo') # mode = pn.stat.mode.to_s(8) # => "100664" # pn.setuid? # => false - # # Set the bit. - # pn.chmod(0o4644) + # pn.chmod(0o4644) # Set the bit. # mode = pn.stat.mode.to_s(8) # => "104644" # pn.setuid? # => true # pn.delete # Clean up. From b20a1ab61b8326591f4248b8cf49d1dbdd28bde3 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Tue, 22 Sep 2026 14:56:57 -0500 Subject: [PATCH 30/31] [DOC] Harmonize file size methods --- file.c | 51 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/file.c b/file.c index 294edcd47040c5..2c10e223b8c9b3 100644 --- a/file.c +++ b/file.c @@ -931,12 +931,32 @@ rb_stat_rdev_minor(VALUE self) } /* + * :markup: markdown + * * call-seq: - * stat.size -> integer + * size -> integer * - * Returns the size of stat in bytes. + * Returns the size of `self` in bytes: + * + * ```ruby + * File.stat('doc/maintainers.md').size # => 14900 # Regular file. + * File.stat('doc/syntax/').size # => 4096 # Directory. + * # When the file size changes. + * path = '/tmp/t.tmp' + * file = File.new(path, 'w+') + * file.write('foo') + * stat = File.stat(path) # Take snapshot. + * stat.size # => 3 + * file.write('bar') # Change file size. + * file.size # => 6 + * stat.size # => 3 # Snapshot unchanged. + * stat = File.stat(path) # Fresh snapshot. + * stat.size # => 6 # Shapshot different. + * # Clean up. + * file.close + * File.delete(path) + * ``` * - * File.stat("testfile").size #=> 66 */ static VALUE @@ -2626,12 +2646,20 @@ rb_file_identical_p(VALUE obj, VALUE fname1, VALUE fname2) } /* + * :markup: markdown + * * call-seq: - * File.size(file_name) -> integer + * File.size(object) -> integer * - * Returns the size of file_name. + * Returns the size in bytes of the given `object`, + * which may be a path or an IO object: + * + * ```ruby + * File.size('doc/maintainers.md') # => 14900 # Regular file. + * File.size('doc/syntax/') # => 4096 # Directory. + * File.size($stdin) # => 0 # IO object. + * ``` * - * _file_name_ can be an IO object. */ static VALUE @@ -3079,12 +3107,17 @@ rb_file_size(VALUE file) } /* + * :markup: markdown + * * call-seq: - * file.size -> integer + * size -> integer * - * Returns the size of file in bytes. + * Returns the size of `self` in bytes: * - * File.new("testfile").size #=> 66 + * ```ruby + * File.new('doc/maintainers.md').size # => 14900 # Regular file. + * File.new('doc/syntax/').size # => 4096 # Directory. + * ``` * */ From bb382ae5df0f1c2d4c7b3964783431175ceef91d Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 22 Sep 2026 16:07:21 +0900 Subject: [PATCH 31/31] Fix use-after-free in Time.new when string modified The precision in Time.new calls to_int which can modify the string passed into Time.new. This can cause an use-after-free. For example, the following script crashes: str = "2000-01-01 00:00:00" + "0" * 1_000_000 obj = Object.new obj.define_singleton_method(:to_int) { str.clear; 9 } Time.new(str, precision: obj) --- test/ruby/test_time.rb | 12 ++++++++++++ time.c | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/test/ruby/test_time.rb b/test/ruby/test_time.rb index fb9864c70e18ce..e4bb82f2f9a465 100644 --- a/test/ruby/test_time.rb +++ b/test/ruby/test_time.rb @@ -166,6 +166,18 @@ def test_new_from_string } end + def test_new_from_string_modified_by_precision + str = "2020-12-25 00:00:00" + "0" * 1_000_000 + obj = Object.new + obj.define_singleton_method(:to_int) do + str.clear + 9 + end + assert_raise_with_message(ArgumentError, /can't parse/) { + Time.new(str, precision: obj) + } + end + def test_time_add() assert_equal(Time.utc(2000, 3, 21, 3, 30) + 3 * 3600, Time.utc(2000, 3, 21, 6, 30)) diff --git a/time.c b/time.c index 1823b1078f0955..ef32b3dd02e8e2 100644 --- a/time.c +++ b/time.c @@ -2655,13 +2655,14 @@ time_init_parse(rb_execution_context_t *ec, VALUE time, VALUE str, VALUE zone, V rb_raise(rb_eArgError, "time string should have ASCII compatible encoding"); } + size_t prec = NIL_P(precision) ? SIZE_MAX : NUM2SIZET(precision); + const char *const begin = RSTRING_PTR(str); const char *const end = RSTRING_END(str); const char *ptr = begin; VALUE year = Qnil, subsec = Qnil; int mon = -1, mday = -1, hour = -1, min = -1, sec = -1; size_t ndigits; - size_t prec = NIL_P(precision) ? SIZE_MAX : NUM2SIZET(precision); if ((ptr < end) && (ISSPACE(*ptr) || ISSPACE(*(end-1)))) { rb_raise(rb_eArgError, "can't parse: %+"PRIsVALUE, str);