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/file.c b/file.c index e12f26e2a918f7..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 @@ -2493,12 +2513,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 @@ -2512,12 +2548,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 @@ -2594,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 @@ -3047,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. + * ``` * */ @@ -7373,14 +7438,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`: + * + * ```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. + * ``` * - * File.stat("/bin/su").setuid? #=> true + * On Windows, the bit is never set; the method always returns `false`. */ static VALUE @@ -7393,15 +7475,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/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/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/pathname_builtin.rb b/pathname_builtin.rb index ad1700b8dc5cd1..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. @@ -2620,17 +2619,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. 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. diff --git a/struct.c b/struct.c index 8bc1694700bdc5..1afd1a4ba4cc04 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); } @@ -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/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index 27511c2f50d0ea..dd2ba5db3ab41b 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 } @@ -582,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' @@ -676,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' @@ -928,6 +947,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 @@ -981,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 @@ -993,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' @@ -1040,9 +1066,11 @@ 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 'missing', File.readlink('tmp/symlink') + assert_file.not_exist?('tmp/symlink') end if have_symlink? and !no_broken_symlink? def test_ln_s_pathname @@ -1229,7 +1257,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" @@ -1885,12 +1913,15 @@ 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 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 @@ -1934,30 +1965,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 . @@ -1969,31 +2014,43 @@ def test_chdir_verbose_frozen def test_getwd check_singleton :getwd + assert_equal(FileUtils.method(:pwd), FileUtils.method(:getwd)) end def test_identical? check_singleton :identical? + assert_equal(FileUtils.method(:cmp), FileUtils.method(:identical?)) end def test_link check_singleton :link + assert_equal(FileUtils.method(:ln), FileUtils.method(:link)) end 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 check_singleton :move + assert_equal(FileUtils.method(:mv), FileUtils.method(:move)) end 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' @@ -2045,14 +2102,17 @@ def test_rmdir def test_rmtree check_singleton :rmtree + assert_equal(FileUtils.method(:rm_rf), FileUtils.method(:rmtree)) end def test_safe_unlink check_singleton :safe_unlink + assert_equal(FileUtils.method(:rm_f), FileUtils.method(:safe_unlink)) end def test_symlink check_singleton :symlink + assert_equal(FileUtils.method(:ln_s), FileUtils.method(:symlink)) end def test_touch @@ -2091,18 +2151,36 @@ 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 + commands = FileUtils.commands + assert_include(commands, 'mv') + assert_include(commands, 'chdir') + assert_equal(commands.uniq, 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 + options = FileUtils.options + assert_include(options, 'force') + assert_include(options, 'verbose') + assert_equal(options.uniq, 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 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/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/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/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); 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 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; } 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; } diff --git a/zjit.c b/zjit.c index 4e44febf72b6bc..e32af670dd29e4 100644 --- a/zjit.c +++ b/zjit.c @@ -345,8 +345,20 @@ 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); +// 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 7da19ed0fa92c2..c4cb49cefd50c1 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,7 +405,9 @@ 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_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/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 1649c1006e1df7..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; @@ -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); @@ -516,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 { @@ -731,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)) @@ -804,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!(), }; @@ -2824,10 +2841,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 @@ -3040,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); @@ -3087,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 @@ -3137,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/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 94d7bd4f24bfe5..dc0aa8698d362e 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,7 +2483,9 @@ 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_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/cruby_methods.rs b/zjit/src/cruby_methods.rs index 4dcd7ca71854c3..99715290e495b2 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -245,11 +245,13 @@ 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); 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 +922,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) }); @@ -1066,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.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 fb4d338f387b5f..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 @@ -12365,6 +12479,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#" @@ -12774,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 "); } @@ -15645,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 @@ -17381,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 @@ -18146,14 +18287,15 @@ mod hir_opt_tests { } #[test] - fn test_print_nil_module_name() { + fn test_fold_class_superclass() { eval(r#" - X = [Module.new].freeze - def test = X[0] + class A; end + class B < A; end + def test = B.superclass test "#); assert_snapshot!(hir_string("test"), @" - fn test@:3: + fn test@:4: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf @@ -18163,111 +18305,23 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint StableConstantNames(0x1000, X) - v11:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v13:Fixnum[0] = Const Value(0) - PatchPoint NoSingletonClass(Array@0x1010) - PatchPoint MethodRedefined(Array@0x1010, []@0x1018, cme:0x1020) - v35:ModuleExact[VALUE(0x1048)] = Const Value(VALUE(0x1048)) + 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 v35 + Return v22 "); } #[test] - fn no_load_from_ep_right_after_entrypoint() { - let formatted = eval(" - def read_nil_local(a, _b, _c) - formatted ||= a - @formatted = formatted - -> { formatted } # the environment escapes - end - - def call - puts [], [], [], [] # fill VM stack with junk - read_nil_local(true, 1, 1) # expected SendDirect - end - - call # profile - call # compile - @formatted - "); - assert_eq!(Qtrue, formatted, "{}", formatted.obj_info()); - assert_snapshot!(hir_string("read_nil_local"), @" - fn read_nil_local@:3: - bb1(): - EntryPoint interpreter - v1:BasicObject = LoadSelf - v2:CPtr = LoadSP - v3:BasicObject = LoadField v2, :a@0x1000 - v4:BasicObject = LoadField v2, :_b@0x1001 - v5:BasicObject = LoadField v2, :_c@0x1002 - Jump bb3(v1, v3, v4, v5) - bb2(): - EntryPoint JIT(0) - v9:BasicObject = LoadArg :self@0 - v10:BasicObject = LoadArg :a@1 - v11:CPtr = GetEP 0 - StoreField v11, :a@0x1001, v10 - v13:BasicObject = LoadArg :_b@2 - StoreField v11, :_b@0x1002, v13 - v15:BasicObject = LoadArg :_c@3 - StoreField v11, :_c@0x1003, v15 - v17:NilClass = Const Value(nil) - StoreField v11, :formatted@0x1004, v17 - Jump bb3(v9, v10, v13, v15) - bb3(v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:BasicObject): - v81:NilClass = Const Value(nil) - SetLocal :formatted, l0, EP@3, v21 - v46:HeapBasicObject = GuardType v20, HeapBasicObject - v47:CShape = LoadField v46, :shape_id@0x1005 - v48:CShape[0x1006] = Const CShape(0x1006) - v49:CBool = IsBitEqual v47, v48 - CondBranch v49, bb7(), bb8() - bb7(): - StoreField v46, :@formatted@0x1007, v21 - WriteBarrier v46, v21 - Jump bb6() - bb8(): - v54:CShape[0x1008] = GuardBitEquals v47, CShape(0x1008) recompile - StoreField v46, :@formatted@0x1007, v21 - WriteBarrier v46, v21 - v58:CShape[0x1006] = Const CShape(0x1006) - StoreField v46, :shape_id@0x1005, v58 - Jump bb6() - bb6(): - v64:ClassSubclass[VMFrozenCore] = Const Value(VALUE(0x1010)) - PatchPoint MethodRedefined(Class@0x1018, lambda@0x1020, cme:0x1028) - v80:BasicObject = CCallWithFrame v64, :RubyVM::FrozenCore.lambda@0x1050, block=0x1058 - v67:CPtr = GetEP 0 - v68:BasicObject = LoadField v67, :a@0x1001 - v69:BasicObject = LoadField v67, :_b@0x1002 - v70:BasicObject = LoadField v67, :_c@0x1003 - v71:BasicObject = LoadField v67, :formatted@0x1004 - CheckInterrupts - Return v80 - "); - } - - #[test] - fn test_fold_load_field_frozen_constant_object() { - // Basic case: frozen constant object with attr_accessor - eval(" - class TestFrozen - attr_accessor :a - def initialize - @a = 1 - end - end - - FROZEN_OBJ = TestFrozen.new.freeze - - def test = FROZEN_OBJ.a - test + fn test_fold_basic_object_superclass() { + eval(r#" + def test = BasicObject.superclass test - "); + "#); assert_snapshot!(hir_string("test"), @" - fn test@:11: + fn test@:2: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf @@ -18277,37 +18331,28 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint StableConstantNames(0x1000, FROZEN_OBJ) - v11:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - PatchPoint NoSingletonClass(TestFrozen@0x1010) - PatchPoint MethodRedefined(TestFrozen@0x1010, a@0x1018, cme:0x1020) - v27:Fixnum[1] = Const Value(1) + 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 v27 + Return v22 "); } #[test] - fn test_fold_load_field_frozen_multiple_ivars() { - // Frozen object with multiple instance variables - eval(" - class TestMultiIvars - attr_accessor :a, :b, :c - def initialize - @a = 10 - @b = 20 - @c = 30 - end + fn test_fold_class_superclass_skips_prepended_module() { + eval(r#" + class A; end + module M; end + class B < A + prepend M end - - MULTI_FROZEN = TestMultiIvars.new.freeze - - def test = MULTI_FROZEN.b - test + def test = B.superclass test - "); + "#); assert_snapshot!(hir_string("test"), @" - fn test@:13: + fn test@:7: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf @@ -18317,29 +18362,689 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - PatchPoint StableConstantNames(0x1000, MULTI_FROZEN) - v11:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - PatchPoint NoSingletonClass(TestMultiIvars@0x1010) - PatchPoint MethodRedefined(TestMultiIvars@0x1010, b@0x1018, cme:0x1020) - v27:Fixnum[20] = Const Value(20) + 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 v27 + Return v22 "); } #[test] - fn test_fold_load_field_frozen_string_value() { - // Frozen object with a string ivar + fn test_fold_singleton_class_superclass() { eval(r#" - class TestFrozenStr - attr_accessor :name - def initialize - @name = "hello" - end - end - - FROZEN_STR = TestFrozenStr.new.freeze - + 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_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#" + X = [Module.new].freeze + def test = X[0] + 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): + PatchPoint StableConstantNames(0x1000, X) + v11:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v13:Fixnum[0] = Const Value(0) + PatchPoint NoSingletonClass(Array@0x1010) + PatchPoint MethodRedefined(Array@0x1010, []@0x1018, cme:0x1020) + v35:ModuleExact[VALUE(0x1048)] = Const Value(VALUE(0x1048)) + CheckInterrupts + Return v35 + "); + } + + #[test] + fn no_load_from_ep_right_after_entrypoint() { + let formatted = eval(" + def read_nil_local(a, _b, _c) + formatted ||= a + @formatted = formatted + -> { formatted } # the environment escapes + end + + def call + puts [], [], [], [] # fill VM stack with junk + read_nil_local(true, 1, 1) # expected SendDirect + end + + call # profile + call # compile + @formatted + "); + assert_eq!(Qtrue, formatted, "{}", formatted.obj_info()); + assert_snapshot!(hir_string("read_nil_local"), @" + fn read_nil_local@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :a@0x1000 + v4:BasicObject = LoadField v2, :_b@0x1001 + v5:BasicObject = LoadField v2, :_c@0x1002 + Jump bb3(v1, v3, v4, v5) + bb2(): + EntryPoint JIT(0) + v9:BasicObject = LoadArg :self@0 + v10:BasicObject = LoadArg :a@1 + v11:CPtr = GetEP 0 + StoreField v11, :a@0x1001, v10 + v13:BasicObject = LoadArg :_b@2 + StoreField v11, :_b@0x1002, v13 + v15:BasicObject = LoadArg :_c@3 + StoreField v11, :_c@0x1003, v15 + v17:NilClass = Const Value(nil) + StoreField v11, :formatted@0x1004, v17 + Jump bb3(v9, v10, v13, v15) + bb3(v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:BasicObject): + v81:NilClass = Const Value(nil) + SetLocal :formatted, l0, EP@3, v21 + v46:HeapBasicObject = GuardType v20, HeapBasicObject + v47:CShape = LoadField v46, :shape_id@0x1005 + v48:CShape[0x1006] = Const CShape(0x1006) + v49:CBool = IsBitEqual v47, v48 + CondBranch v49, bb7(), bb8() + bb7(): + StoreField v46, :@formatted@0x1007, v21 + WriteBarrier v46, v21 + Jump bb6() + bb8(): + v54:CShape[0x1008] = GuardBitEquals v47, CShape(0x1008) recompile + StoreField v46, :@formatted@0x1007, v21 + WriteBarrier v46, v21 + v58:CShape[0x1006] = Const CShape(0x1006) + StoreField v46, :shape_id@0x1005, v58 + Jump bb6() + bb6(): + v64:ClassSubclass[VMFrozenCore] = Const Value(VALUE(0x1010)) + PatchPoint MethodRedefined(Class@0x1018, lambda@0x1020, cme:0x1028) + v80:BasicObject = CCallWithFrame v64, :RubyVM::FrozenCore.lambda@0x1050, block=0x1058 + v67:CPtr = GetEP 0 + v68:BasicObject = LoadField v67, :a@0x1001 + v69:BasicObject = LoadField v67, :_b@0x1002 + v70:BasicObject = LoadField v67, :_c@0x1003 + v71:BasicObject = LoadField v67, :formatted@0x1004 + CheckInterrupts + Return v80 + "); + } + + #[test] + fn test_fold_load_field_frozen_constant_object() { + // Basic case: frozen constant object with attr_accessor + eval(" + class TestFrozen + attr_accessor :a + def initialize + @a = 1 + end + end + + FROZEN_OBJ = TestFrozen.new.freeze + + def test = FROZEN_OBJ.a + test + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:11: + 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, FROZEN_OBJ) + v11:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint NoSingletonClass(TestFrozen@0x1010) + PatchPoint MethodRedefined(TestFrozen@0x1010, a@0x1018, cme:0x1020) + v27:Fixnum[1] = Const Value(1) + CheckInterrupts + Return v27 + "); + } + + #[test] + fn test_fold_load_field_frozen_multiple_ivars() { + // Frozen object with multiple instance variables + eval(" + class TestMultiIvars + attr_accessor :a, :b, :c + def initialize + @a = 10 + @b = 20 + @c = 30 + end + end + + MULTI_FROZEN = TestMultiIvars.new.freeze + + def test = MULTI_FROZEN.b + test + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:13: + 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, MULTI_FROZEN) + v11:ObjectSubclass[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + PatchPoint NoSingletonClass(TestMultiIvars@0x1010) + PatchPoint MethodRedefined(TestMultiIvars@0x1010, b@0x1018, cme:0x1020) + v27:Fixnum[20] = Const Value(20) + CheckInterrupts + Return v27 + "); + } + + #[test] + fn test_fold_load_field_frozen_string_value() { + // Frozen object with a string ivar + eval(r#" + class TestFrozenStr + attr_accessor :name + def initialize + @name = "hello" + end + end + + FROZEN_STR = TestFrozenStr.new.freeze + def test = FROZEN_STR.name test test @@ -19708,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 "); } @@ -19762,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 @@ -19818,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 "); } @@ -19858,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 @@ -19923,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 @@ -19976,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 @@ -20029,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 @@ -20086,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 @@ -21603,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 @@ -21895,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) @@ -21919,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 @@ -23862,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 @@ -23994,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) {