diff --git a/.github/actions/setup/macos/action.yml b/.github/actions/setup/macos/action.yml index cfb456c2c033a3..3649a648760324 100644 --- a/.github/actions/setup/macos/action.yml +++ b/.github/actions/setup/macos/action.yml @@ -13,7 +13,6 @@ runs: - name: brew shell: bash run: | - brew upgrade --quiet brew install --quiet gmp libffi openssl@1.1 zlib autoconf automake libtool readline - name: Set ENV diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb index 6cc509f212a959..f90cece848649c 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -4004,3 +4004,25 @@ def calling_func _x, _y = func.call end.call } + +# Catch TAG_BREAK in a non-FINISH frame with JIT code +assert_equal '1', %q{ + def entry + catch_break + end + + def catch_break + while_true do + break + end + 1 + end + + def while_true + while true + yield + end + end + + entry +} diff --git a/gc.c b/gc.c index 8913f15d61be10..70b9b36d638d38 100644 --- a/gc.c +++ b/gc.c @@ -1492,9 +1492,6 @@ asan_poison_object_restore(VALUE obj, void *ptr) #define RVALUE_PAGE_UNCOLLECTIBLE(page, obj) MARKED_IN_BITMAP((page)->uncollectible_bits, (obj)) #define RVALUE_PAGE_MARKING(page, obj) MARKED_IN_BITMAP((page)->marking_bits, (obj)) - -static int rgengc_remembered(rb_objspace_t *objspace, VALUE obj); -static int rgengc_remembered_sweep(rb_objspace_t *objspace, VALUE obj); static int rgengc_remember(rb_objspace_t *objspace, VALUE obj); static void rgengc_mark_and_rememberset_clear(rb_objspace_t *objspace, rb_heap_t *heap); static void rgengc_rememberset_mark(rb_objspace_t *objspace, rb_heap_t *heap); @@ -2507,7 +2504,7 @@ newobj_init(VALUE klass, VALUE flags, int wb_protected, rb_objspace_t *objspace, GC_ASSERT(RVALUE_OLD_P(obj) == FALSE); GC_ASSERT(RVALUE_WB_UNPROTECTED(obj) == FALSE); - if (rgengc_remembered(objspace, (VALUE)obj)) rb_bug("newobj: %s is remembered.", obj_info(obj)); + if (RVALUE_REMEMBERED((VALUE)obj)) rb_bug("newobj: %s is remembered.", obj_info(obj)); } RB_VM_LOCK_LEAVE_NO_BARRIER(); #endif @@ -5435,7 +5432,7 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit #if RGENGC_CHECK_MODE if (!is_full_marking(objspace)) { if (RVALUE_OLD_P(vp)) rb_bug("page_sweep: %p - old while minor GC.", (void *)p); - if (rgengc_remembered_sweep(objspace, vp)) rb_bug("page_sweep: %p - remembered.", (void *)p); + if (RVALUE_REMEMBERED(vp)) rb_bug("page_sweep: %p - remembered.", (void *)p); } #endif if (obj_free(objspace, vp)) { @@ -8578,12 +8575,6 @@ gc_report_body(int level, rb_objspace_t *objspace, const char *fmt, ...) /* bit operations */ -static int -rgengc_remembersetbits_get(rb_objspace_t *objspace, VALUE obj) -{ - return RVALUE_REMEMBERED(obj); -} - static int rgengc_remembersetbits_set(rb_objspace_t *objspace, VALUE obj) { @@ -8607,7 +8598,7 @@ static int rgengc_remember(rb_objspace_t *objspace, VALUE obj) { gc_report(6, objspace, "rgengc_remember: %s %s\n", obj_info(obj), - rgengc_remembersetbits_get(objspace, obj) ? "was already remembered" : "is remembered now"); + RVALUE_REMEMBERED(obj) ? "was already remembered" : "is remembered now"); check_rvalue_consistency(obj); @@ -8616,7 +8607,7 @@ rgengc_remember(rb_objspace_t *objspace, VALUE obj) } #if RGENGC_PROFILE > 0 - if (!rgengc_remembered(objspace, obj)) { + if (!RVALUE_REMEMBERED(obj)) { if (RVALUE_WB_UNPROTECTED(obj) == 0) { objspace->profile.total_remembered_normal_object_count++; #if RGENGC_PROFILE >= 2 @@ -8629,21 +8620,6 @@ rgengc_remember(rb_objspace_t *objspace, VALUE obj) return rgengc_remembersetbits_set(objspace, obj); } -static int -rgengc_remembered_sweep(rb_objspace_t *objspace, VALUE obj) -{ - int result = rgengc_remembersetbits_get(objspace, obj); - check_rvalue_consistency(obj); - return result; -} - -static int -rgengc_remembered(rb_objspace_t *objspace, VALUE obj) -{ - gc_report(6, objspace, "rgengc_remembered: %s\n", obj_info(obj)); - return rgengc_remembered_sweep(objspace, obj); -} - #ifndef PROFILE_REMEMBERSET_MARK #define PROFILE_REMEMBERSET_MARK 0 #endif @@ -8749,7 +8725,7 @@ gc_writebarrier_generational(VALUE a, VALUE b, rb_objspace_t *objspace) } /* mark `a' and remember (default behavior) */ - if (!rgengc_remembered(objspace, a)) { + if (!RVALUE_REMEMBERED(a)) { RB_VM_LOCK_ENTER_NO_BARRIER(); { rgengc_remember(objspace, a); @@ -8844,7 +8820,7 @@ rb_gc_writebarrier_unprotect(VALUE obj) rb_objspace_t *objspace = &rb_objspace; gc_report(2, objspace, "rb_gc_writebarrier_unprotect: %s %s\n", obj_info(obj), - rgengc_remembered(objspace, obj) ? " (already remembered)" : ""); + RVALUE_REMEMBERED(obj) ? " (already remembered)" : ""); RB_VM_LOCK_ENTER_NO_BARRIER(); { diff --git a/lib/ruby_vm/rjit/compiler.rb b/lib/ruby_vm/rjit/compiler.rb index 1c024c4c40611a..9ae528a0e99d4c 100644 --- a/lib/ruby_vm/rjit/compiler.rb +++ b/lib/ruby_vm/rjit/compiler.rb @@ -64,7 +64,7 @@ def compile(iseq, cfp) asm = Assembler.new compile_prologue(asm, iseq, pc) compile_block(asm, jit:, pc:) - iseq.body.jit_func = @cb.write(asm) + iseq.body.jit_entry = @cb.write(asm) rescue Exception => e $stderr.puts e.full_message exit 1 @@ -176,8 +176,8 @@ def invalidate_blocks(iseq, pc) # If they were the ISEQ's first blocks, re-compile RJIT entry as well if iseq.body.iseq_encoded.to_i == pc - iseq.body.jit_func = 0 - iseq.body.total_calls = 0 + iseq.body.jit_entry = 0 + iseq.body.jit_entry_calls = 0 end end diff --git a/lib/ruby_vm/rjit/invariants.rb b/lib/ruby_vm/rjit/invariants.rb index db1068b5c29935..5b061d19944dca 100644 --- a/lib/ruby_vm/rjit/invariants.rb +++ b/lib/ruby_vm/rjit/invariants.rb @@ -143,11 +143,11 @@ def invalidate_all C.rjit_for_each_iseq do |iseq| # Avoid entering past code - iseq.body.jit_func = 0 + iseq.body.jit_entry = 0 # Avoid reusing past code iseq.body.rjit_blocks.clear if iseq.body.rjit_blocks # Compile this again if not converted to trace_* insns - iseq.body.total_calls = 0 + iseq.body.jit_entry_calls = 0 end end end diff --git a/object.c b/object.c index 823b8df2cf6305..45a571adacea35 100644 --- a/object.c +++ b/object.c @@ -2165,6 +2165,10 @@ rb_class_superclass(VALUE klass) if (klass == rb_cBasicObject) return Qnil; rb_raise(rb_eTypeError, "uninitialized class"); } + + if (!RCLASS_SUPERCLASS_DEPTH(klass)) { + return Qnil; + } else { super = RCLASS_SUPERCLASSES(klass)[RCLASS_SUPERCLASS_DEPTH(klass) - 1]; RUBY_ASSERT(RB_TYPE_P(klass, T_CLASS)); diff --git a/rjit_c.rb b/rjit_c.rb index 9e0ea84f0aab5f..78d1a6cf747e62 100644 --- a/rjit_c.rb +++ b/rjit_c.rb @@ -1177,8 +1177,8 @@ def C.rb_iseq_constant_body ), Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), mark_bits)")], outer_variables: [CType::Pointer.new { self.rb_id_table }, Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), outer_variables)")], mandatory_only_iseq: [CType::Pointer.new { self.rb_iseq_t }, Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), mandatory_only_iseq)")], - jit_func: [self.rb_jit_func_t, Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), jit_func)")], - total_calls: [CType::Immediate.parse("unsigned long"), Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), total_calls)")], + jit_entry: [self.rb_jit_func_t, Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), jit_entry)")], + jit_entry_calls: [CType::Immediate.parse("unsigned long"), Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), jit_entry_calls)")], rjit_blocks: [self.VALUE, Primitive.cexpr!("OFFSETOF((*((struct rb_iseq_constant_body *)NULL)), rjit_blocks)"), true], ) end diff --git a/test/ruby/test_class.rb b/test/ruby/test_class.rb index 98acbaf67b70cc..a8a019cee2d9e6 100644 --- a/test/ruby/test_class.rb +++ b/test/ruby/test_class.rb @@ -96,6 +96,13 @@ def test_instantiate_singleton_class def test_superclass_of_basicobject assert_equal(nil, BasicObject.superclass) + + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + module Mod end + BasicObject.include(Mod) + assert_equal(nil, BasicObject.superclass) + end; end def test_module_function diff --git a/tool/rjit/bindgen.rb b/tool/rjit/bindgen.rb index f54d2203ac3531..0b4b2fa09582e9 100755 --- a/tool/rjit/bindgen.rb +++ b/tool/rjit/bindgen.rb @@ -638,7 +638,7 @@ def push_target(target) skip_fields: { 'rb_execution_context_struct.machine': %w[regs], # differs between macOS and Linux rb_execution_context_struct: %w[method_missing_reason], # non-leading bit fields not supported - rb_iseq_constant_body: %w[yjit_payload], # conditionally defined + rb_iseq_constant_body: %w[jit_exception jit_exception_calls yjit_payload], # conditionally defined rb_thread_struct: %w[status has_dedicated_nt to_kill abort_on_exception report_on_exception pending_interrupt_queue_checked], :'' => %w[is_from_method is_lambda is_isolated], # rb_proc_t }, diff --git a/vm.c b/vm.c index 1a14b48e679fb8..106155b50fe4e9 100644 --- a/vm.c +++ b/vm.c @@ -370,7 +370,14 @@ extern VALUE rb_vm_invoke_bmethod(rb_execution_context_t *ec, rb_proc_t *proc, V static VALUE vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE block_handler); #if USE_RJIT || USE_YJIT -// Try to compile the current ISeq in ec. Return 0 if not compiled. +// Generate JIT code that supports the following kinds of ISEQ entries: +// * The first ISEQ on vm_exec (e.g.
, or Ruby methods/blocks +// called by a C method). The current frame has VM_FRAME_FLAG_FINISH. +// The current vm_exec stops if JIT code returns a non-Qundef value. +// * ISEQs called by the interpreter on vm_sendish (e.g. Ruby methods or +// blocks called by a Ruby frame that isn't compiled or side-exited). +// The current frame doesn't have VM_FRAME_FLAG_FINISH. The current +// vm_exec does NOT stop whether JIT code returns Qundef or not. static inline rb_jit_func_t jit_compile(rb_execution_context_t *ec) { @@ -379,35 +386,29 @@ jit_compile(rb_execution_context_t *ec) struct rb_iseq_constant_body *body = ISEQ_BODY(iseq); bool yjit_enabled = rb_yjit_compile_new_iseqs(); if (yjit_enabled || rb_rjit_call_p) { - body->total_calls++; + body->jit_entry_calls++; } else { - return 0; - } - - // Don't try to compile the function if it's already compiled - if (body->jit_func) { - return body->jit_func; + return NULL; } - // Trigger JIT compilation as needed - if (yjit_enabled) { - if (rb_yjit_threshold_hit(iseq)) { - rb_yjit_compile_iseq(iseq, ec); + // Trigger JIT compilation if not compiled + if (body->jit_entry == NULL) { + if (yjit_enabled) { + if (rb_yjit_threshold_hit(iseq, body->jit_entry_calls)) { + rb_yjit_compile_iseq(iseq, ec, false); + } } - } - else { // rb_rjit_call_p - if (body->total_calls == rb_rjit_call_threshold()) { - rb_rjit_compile(iseq); + else { // rb_rjit_call_p + if (body->jit_entry_calls == rb_rjit_call_threshold()) { + rb_rjit_compile(iseq); + } } } - - return body->jit_func; + return body->jit_entry; } -// Try to execute the current iseq in ec. Use JIT code if it is ready. -// If it is not, add ISEQ to the compilation queue and return Qundef for RJIT. -// YJIT compiles on the thread running the iseq. +// Execute JIT code compiled by jit_compile() static inline VALUE jit_exec(rb_execution_context_t *ec) { @@ -425,6 +426,51 @@ jit_exec(rb_execution_context_t *ec) # define jit_exec(ec) Qundef #endif +#if USE_YJIT +// Generate JIT code that supports the following kind of ISEQ entry: +// * The first ISEQ pushed by vm_exec_handle_exception. The frame would +// point to a location specified by a catch table, and it doesn't have +// VM_FRAME_FLAG_FINISH. The current vm_exec stops if JIT code returns +// a non-Qundef value. So you should not return a non-Qundef value +// until ec->cfp is changed to a frame with VM_FRAME_FLAG_FINISH. +static inline rb_jit_func_t +jit_compile_exception(rb_execution_context_t *ec) +{ + // Increment the ISEQ's call counter + const rb_iseq_t *iseq = ec->cfp->iseq; + struct rb_iseq_constant_body *body = ISEQ_BODY(iseq); + if (rb_yjit_compile_new_iseqs()) { + body->jit_exception_calls++; + } + else { + return NULL; + } + + // Trigger JIT compilation if not compiled + if (body->jit_exception == NULL && rb_yjit_threshold_hit(iseq, body->jit_exception_calls)) { + rb_yjit_compile_iseq(iseq, ec, true); + } + return body->jit_exception; +} + +// Execute JIT code compiled by jit_compile_exception() +static inline VALUE +jit_exec_exception(rb_execution_context_t *ec) +{ + rb_jit_func_t func = jit_compile_exception(ec); + if (func) { + // Call the JIT code + return func(ec, ec->cfp); + } + else { + return Qundef; + } +} +#else +# define jit_compile_exception(ec) ((rb_jit_func_t)0) +# define jit_exec_exception(ec) Qundef +#endif + #include "vm_insnhelper.c" #include "vm_exec.c" @@ -2381,8 +2427,11 @@ vm_exec_loop(rb_execution_context_t *ec, enum ruby_tag_type state, rb_ec_raised_reset(ec, RAISED_STACKOVERFLOW | RAISED_NOMEMORY); while (UNDEF_P(result = vm_exec_handle_exception(ec, state, result))) { - /* caught a jump, exec the handler */ - result = vm_exec_core(ec); + // caught a jump, exec the handler. JIT code in jit_exec_exception() + // may return Qundef to run remaining frames with vm_exec_core(). + if (UNDEF_P(result = jit_exec_exception(ec))) { + result = vm_exec_core(ec); + } vm_loop_start: VM_ASSERT(ec->tag == tag); /* when caught `throw`, `tag.state` is set. */ diff --git a/vm_core.h b/vm_core.h index 77861372ffc076..d9d926d2ad5ba0 100644 --- a/vm_core.h +++ b/vm_core.h @@ -503,10 +503,17 @@ struct rb_iseq_constant_body { const rb_iseq_t *mandatory_only_iseq; #if USE_RJIT || USE_YJIT - // Function pointer for JIT code - rb_jit_func_t jit_func; - // Number of total calls with jit_exec() - long unsigned total_calls; + // Function pointer for JIT code on jit_exec() + rb_jit_func_t jit_entry; + // Number of calls on jit_exec() + long unsigned jit_entry_calls; +#endif + +#if USE_YJIT + // Function pointer for JIT code on jit_exec_exception() + rb_jit_func_t jit_exception; + // Number of calls on jit_exec_exception() + long unsigned jit_exception_calls; #endif #if USE_RJIT diff --git a/vm_insnhelper.c b/vm_insnhelper.c index b226e1397e9498..5cbd47d78b23e9 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -2485,6 +2485,12 @@ vm_base_ptr(const rb_control_frame_t *cfp) } } +VALUE * +rb_vm_base_ptr(const rb_control_frame_t *cfp) +{ + return vm_base_ptr(cfp); +} + /* method call processes with call_info */ #include "vm_args.c" diff --git a/yarp/api_pack.c b/yarp/api_pack.c index d089f8cecaab95..7f0440970e3514 100644 --- a/yarp/api_pack.c +++ b/yarp/api_pack.c @@ -9,6 +9,16 @@ static VALUE v3_2_0_symbol; static VALUE pack_symbol; static VALUE unpack_symbol; +#if SIZEOF_UINT64_T == SIZEOF_LONG_LONG +# define UINT64T2NUM(x) ULL2NUM(x) +# define NUM2UINT64T(x) (uint64_t)NUM2ULL(x) +#elif SIZEOF_UINT64_T == SIZEOF_LONG +# define UINT64T2NUM(x) ULONG2NUM(x) +# define NUM2UINT64T(x) (uint64_t)NUM2ULONG(x) +#else +// error No uint64_t conversion +#endif + static VALUE pack_type_to_symbol(yp_pack_type type) { switch (type) { @@ -188,7 +198,7 @@ pack_parse(VALUE self, VALUE version_symbol, VALUE variant_symbol, VALUE format_ const char *directive_start = format; yp_pack_result parse_result = yp_pack_parse(variant, &format, format_end, &type, &signed_type, &endian, - &size, &length_type, &length, &encoding); + &size, &length_type, &length, &encoding); const char *directive_end = format; @@ -214,14 +224,14 @@ pack_parse(VALUE self, VALUE version_symbol, VALUE variant_symbol, VALUE format_ } VALUE directive_args[9] = { version_symbol, - variant_symbol, - rb_usascii_str_new(directive_start, directive_end - directive_start), - pack_type_to_symbol(type), - pack_signed_to_symbol(signed_type), - pack_endian_to_symbol(endian), - pack_size_to_symbol(size), - pack_length_type_to_symbol(length_type), - (long) LONG2NUM(length) }; + variant_symbol, + rb_usascii_str_new(directive_start, directive_end - directive_start), + pack_type_to_symbol(type), + pack_signed_to_symbol(signed_type), + pack_endian_to_symbol(endian), + pack_size_to_symbol(size), + pack_length_type_to_symbol(length_type), + UINT64T2NUM(length) }; rb_ary_push(directives_array, rb_class_new_instance(9, directive_args, rb_cYARPPackDirective)); } diff --git a/yjit.c b/yjit.c index 132c2f09597fae..052346950f1883 100644 --- a/yjit.c +++ b/yjit.c @@ -422,10 +422,12 @@ void rb_iseq_reset_jit_func(const rb_iseq_t *iseq) { RUBY_ASSERT_ALWAYS(IMEMO_TYPE_P(iseq, imemo_iseq)); - iseq->body->jit_func = NULL; + iseq->body->jit_entry = NULL; + iseq->body->jit_exception = NULL; // Enable re-compiling this ISEQ. Event when it's invalidated for TracePoint, // we'd like to re-compile ISEQs that haven't been converted to trace_* insns. - iseq->body->total_calls = 0; + iseq->body->jit_entry_calls = 0; + iseq->body->jit_exception_calls = 0; } // Get the PC for a given index in an iseq @@ -597,12 +599,6 @@ rb_get_def_bmethod_proc(rb_method_definition_t *def) return def->body.bmethod.proc; } -unsigned long -rb_get_iseq_body_total_calls(const rb_iseq_t *iseq) -{ - return iseq->body->total_calls; -} - const rb_iseq_t * rb_get_iseq_body_local_iseq(const rb_iseq_t *iseq) { @@ -832,6 +828,8 @@ rb_get_cfp_ep_level(struct rb_control_frame_struct *cfp, uint32_t lv) return ep; } +extern VALUE *rb_vm_base_ptr(struct rb_control_frame_struct *cfp); + VALUE rb_yarv_class_of(VALUE obj) { @@ -1047,27 +1045,24 @@ rb_yjit_vm_unlock(unsigned int *recursive_lock_level, const char *file, int line rb_vm_lock_leave(recursive_lock_level, file, line); } -bool -rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec) +void +rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception) { - bool success = true; RB_VM_LOCK_ENTER(); rb_vm_barrier(); - // Compile a block version starting at the first instruction - uint8_t *rb_yjit_iseq_gen_entry_point(const rb_iseq_t *iseq, rb_execution_context_t *ec); // defined in Rust - uint8_t *code_ptr = rb_yjit_iseq_gen_entry_point(iseq, ec); + // Compile a block version starting at the current instruction + uint8_t *rb_yjit_iseq_gen_entry_point(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception); // defined in Rust + uint8_t *code_ptr = rb_yjit_iseq_gen_entry_point(iseq, ec, jit_exception); - if (code_ptr) { - iseq->body->jit_func = (rb_jit_func_t)code_ptr; + if (jit_exception) { + iseq->body->jit_exception = (rb_jit_func_t)code_ptr; } else { - iseq->body->jit_func = 0; - success = false; + iseq->body->jit_entry = (rb_jit_func_t)code_ptr; } RB_VM_LOCK_LEAVE(); - return success; } // GC root for interacting with the GC @@ -1143,6 +1138,35 @@ rb_yjit_invokeblock_sp_pops(const struct rb_callinfo *ci) return 1 - sp_inc_of_invokeblock(ci); // + 1 to ignore return value push } +// Setup jit_return to avoid returning a non-Qundef value on a non-FINISH frame. +// See [jit_compile_exception] for details. +void +rb_yjit_set_exception_return(rb_control_frame_t *cfp, void *leave_exit, void *leave_exception) +{ + if (VM_FRAME_FINISHED_P(cfp)) { + // If it's a FINISH frame, just normally exit with a non-Qundef value. + cfp->jit_return = leave_exit; + } + else if (cfp->jit_return) { + while (!VM_FRAME_FINISHED_P(cfp)) { + if (cfp->jit_return == leave_exit) { + // Unlike jit_exec(), leave_exit is not safe on a non-FINISH frame on + // jit_exec_exception(). See [jit_exec] and [jit_exec_exception] for + // details. Exit to the interpreter with Qundef to let it keep executing + // other Ruby frames. + cfp->jit_return = leave_exception; + return; + } + cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp); + } + } + else { + // If the caller was not JIT code, exit to the interpreter with Qundef + // to keep executing Ruby frames with the interpreter. + cfp->jit_return = leave_exception; + } +} + // Primitives used by yjit.rb VALUE rb_yjit_stats_enabled_p(rb_execution_context_t *ec, VALUE self); VALUE rb_yjit_trace_exit_locations_enabled_p(rb_execution_context_t *ec, VALUE self); diff --git a/yjit.h b/yjit.h index 37a3fb0d4911dd..d54bd7e0eab386 100644 --- a/yjit.h +++ b/yjit.h @@ -27,12 +27,12 @@ // Expose these as declarations since we are building YJIT. bool rb_yjit_enabled_p(void); bool rb_yjit_compile_new_iseqs(void); -bool rb_yjit_threshold_hit(const rb_iseq_t *const iseq); +bool rb_yjit_threshold_hit(const rb_iseq_t *const iseq, unsigned long total_calls); void rb_yjit_invalidate_all_method_lookup_assumptions(void); void rb_yjit_cme_invalidate(rb_callable_method_entry_t *cme); void rb_yjit_collect_binding_alloc(void); void rb_yjit_collect_binding_set(void); -bool rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec); +void rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception); void rb_yjit_init(void); void rb_yjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop); void rb_yjit_constant_state_changed(ID id); @@ -49,12 +49,12 @@ void rb_yjit_tracing_invalidate_all(void); static inline bool rb_yjit_enabled_p(void) { return false; } static inline bool rb_yjit_compile_new_iseqs(void) { return false; } -static inline bool rb_yjit_threshold_hit(const rb_iseq_t *const iseq) { return false; } +static inline bool rb_yjit_threshold_hit(const rb_iseq_t *const iseq, unsigned long total_calls) { return false; } static inline void rb_yjit_invalidate_all_method_lookup_assumptions(void) {} static inline void rb_yjit_cme_invalidate(rb_callable_method_entry_t *cme) {} static inline void rb_yjit_collect_binding_alloc(void) {} static inline void rb_yjit_collect_binding_set(void) {} -static inline bool rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec) { return false; } +static inline void rb_yjit_compile_iseq(const rb_iseq_t *iseq, rb_execution_context_t *ec, bool jit_exception) {} static inline void rb_yjit_init(void) {} static inline void rb_yjit_bop_redefined(int redefined_flag, enum ruby_basic_operators bop) {} static inline void rb_yjit_constant_state_changed(ID id) {} diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index d83e9487767cfd..29f39a7cb4084e 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -326,6 +326,7 @@ fn main() { .allowlist_function("rb_yjit_assert_holding_vm_lock") .allowlist_function("rb_yjit_sendish_sp_pops") .allowlist_function("rb_yjit_invokeblock_sp_pops") + .allowlist_function("rb_yjit_set_exception_return") .allowlist_type("robject_offsets") .allowlist_type("rstring_offsets") @@ -443,6 +444,7 @@ fn main() { .allowlist_function("rb_yjit_array_len") .allowlist_function("rb_obj_class") .allowlist_function("rb_obj_is_proc") + .allowlist_function("rb_vm_base_ptr") // We define VALUE manually, don't import it .blocklist_type("VALUE") diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 721b2a9b071093..39fc3fb569005b 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -18,7 +18,7 @@ use std::cmp::min; use std::collections::HashMap; use std::ffi::CStr; use std::mem; -use std::os::raw::{c_int}; +use std::os::raw::c_int; use std::ptr; use std::rc::Rc; use std::slice; @@ -405,7 +405,7 @@ fn verify_ctx(jit: &JITState, ctx: &Context) { // to the interpreter when it cannot service a stub by generating new code. // Before coming here, branch_stub_hit() takes care of fully reconstructing // interpreter state. -fn gen_code_for_exit_from_stub(ocb: &mut OutlinedCb) -> CodePtr { +fn gen_stub_exit(ocb: &mut OutlinedCb) -> CodePtr { let ocb = ocb.unwrap(); let code_ptr = ocb.get_write_ptr(); let mut asm = Assembler::new(); @@ -617,6 +617,38 @@ fn gen_leave_exit(ocb: &mut OutlinedCb) -> CodePtr { return code_ptr; } +// Increment SP and transfer the execution to the interpreter after jit_exec_exception(). +// On jit_exec_exception(), you need to return Qundef to keep executing caller non-FINISH +// frames on the interpreter. You also need to increment SP to push the return value to +// the caller's stack, which is different from gen_stub_exit(). +fn gen_leave_exception(ocb: &mut OutlinedCb) -> CodePtr { + let ocb = ocb.unwrap(); + let code_ptr = ocb.get_write_ptr(); + let mut asm = Assembler::new(); + + // Every exit to the interpreter should be counted + gen_counter_incr(&mut asm, Counter::leave_interp_return); + + asm.comment("increment SP of the caller"); + let sp = Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SP); + let new_sp = asm.add(sp, SIZEOF_VALUE.into()); + asm.mov(sp, new_sp); + + asm.comment("exit from exception"); + asm.cpop_into(SP); + asm.cpop_into(EC); + asm.cpop_into(CFP); + + asm.frame_teardown(); + + // Execute vm_exec_core + asm.cret(Qundef.into()); + + asm.compile(ocb, None); + + return code_ptr; +} + // Generate a runtime guard that ensures the PC is at the expected // instruction index in the iseq, otherwise takes an entry stub // that generates another check and entry. @@ -647,7 +679,15 @@ pub fn gen_entry_chain_guard( /// Compile an interpreter entry block to be inserted into an iseq /// Returns None if compilation fails. -pub fn gen_entry_prologue(cb: &mut CodeBlock, ocb: &mut OutlinedCb, iseq: IseqPtr, insn_idx: u16) -> Option { +/// If jit_exception is true, compile JIT code for handling exceptions. +/// See [jit_compile_exception] for details. +pub fn gen_entry_prologue( + cb: &mut CodeBlock, + ocb: &mut OutlinedCb, + iseq: IseqPtr, + insn_idx: u16, + jit_exception: bool, +) -> Option { let code_ptr = cb.get_write_ptr(); let mut asm = Assembler::new(); @@ -672,19 +712,36 @@ pub fn gen_entry_prologue(cb: &mut CodeBlock, ocb: &mut OutlinedCb, iseq: IseqPt asm.mov(SP, Opnd::mem(64, CFP, RUBY_OFFSET_CFP_SP)); // Setup cfp->jit_return - asm.mov( - Opnd::mem(64, CFP, RUBY_OFFSET_CFP_JIT_RETURN), - Opnd::const_ptr(CodegenGlobals::get_leave_exit_code().raw_ptr()), - ); + // If this is an exception handler entry point + if jit_exception { + // On jit_exec_exception(), it's NOT safe to return a non-Qundef value + // from a non-FINISH frame. This function fixes that problem. + // See [jit_compile_exception] for details. + asm.ccall( + rb_yjit_set_exception_return as *mut u8, + vec![ + CFP, + Opnd::const_ptr(CodegenGlobals::get_leave_exit_code().raw_ptr()), + Opnd::const_ptr(CodegenGlobals::get_leave_exception_code().raw_ptr()), + ], + ); + } else { + // On jit_exec() or JIT_EXEC(), it's safe to return a non-Qundef value + // on the entry frame. See [jit_compile] for details. + asm.mov( + Opnd::mem(64, CFP, RUBY_OFFSET_CFP_JIT_RETURN), + Opnd::const_ptr(CodegenGlobals::get_leave_exit_code().raw_ptr()), + ); + } - // We're compiling iseqs that we *expect* to start at `insn_idx`. But in - // the case of optional parameters, the interpreter can set the pc to a - // different location depending on the optional parameters. If an iseq - // has optional parameters, we'll add a runtime check that the PC we've + // We're compiling iseqs that we *expect* to start at `insn_idx`. + // But in the case of optional parameters or when handling exceptions, + // the interpreter can set the pc to a different location. For + // such scenarios, we'll add a runtime check that the PC we've // compiled for is the same PC that the interpreter wants us to run with. // If they don't match, then we'll jump to an entry stub and generate // another PC check and entry there. - let pending_entry = if unsafe { get_iseq_flags_has_opt(iseq) } { + let pending_entry = if unsafe { get_iseq_flags_has_opt(iseq) } || jit_exception { Some(gen_entry_chain_guard(&mut asm, ocb, iseq, insn_idx)?) } else { None @@ -8283,8 +8340,11 @@ pub struct CodegenGlobals { /// Code for exiting back to the interpreter from the leave instruction leave_exit_code: CodePtr, + /// Code for exiting back to the interpreter after handling an exception + leave_exception_code: CodePtr, + // For exiting from YJIT frame from branch_stub_hit(). - // Filled by gen_code_for_exit_from_stub(). + // Filled by gen_stub_exit(). stub_exit_code: CodePtr, // For servicing branch stubs @@ -8373,8 +8433,9 @@ impl CodegenGlobals { let ocb_start_addr = ocb.unwrap().get_write_ptr(); let leave_exit_code = gen_leave_exit(&mut ocb); + let leave_exception_code = gen_leave_exception(&mut ocb); - let stub_exit_code = gen_code_for_exit_from_stub(&mut ocb); + let stub_exit_code = gen_stub_exit(&mut ocb); let branch_stub_hit_trampoline = gen_branch_stub_hit_trampoline(&mut ocb); let entry_stub_hit_trampoline = gen_entry_stub_hit_trampoline(&mut ocb); @@ -8393,7 +8454,8 @@ impl CodegenGlobals { inline_cb: cb, outlined_cb: ocb, leave_exit_code, - stub_exit_code: stub_exit_code, + leave_exception_code, + stub_exit_code, outline_full_cfunc_return_pos: cfunc_exit_code, branch_stub_hit_trampoline, entry_stub_hit_trampoline, @@ -8513,6 +8575,10 @@ impl CodegenGlobals { CodegenGlobals::get_instance().leave_exit_code } + pub fn get_leave_exception_code() -> CodePtr { + CodegenGlobals::get_instance().leave_exception_code + } + pub fn get_stub_exit_code() -> CodePtr { CodegenGlobals::get_instance().stub_exit_code } diff --git a/yjit/src/core.rs b/yjit/src/core.rs index 32324deb97cb0d..89061ac4e5ecec 100644 --- a/yjit/src/core.rs +++ b/yjit/src/core.rs @@ -2135,12 +2135,18 @@ fn gen_block_series_body( /// Generate a block version that is an entry point inserted into an iseq /// NOTE: this function assumes that the VM lock has been taken -pub fn gen_entry_point(iseq: IseqPtr, ec: EcPtr) -> Option { +/// If jit_exception is true, compile JIT code for handling exceptions. +/// See [jit_compile_exception] for details. +pub fn gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exception: bool) -> Option { // Compute the current instruction index based on the current PC + let cfp = unsafe { get_ec_cfp(ec) }; let insn_idx: u16 = unsafe { - let ec_pc = get_cfp_pc(get_ec_cfp(ec)); + let ec_pc = get_cfp_pc(cfp); iseq_pc_to_insn_idx(iseq, ec_pc)? }; + let stack_size: u8 = unsafe { + u8::try_from(get_cfp_sp(cfp).offset_from(get_cfp_bp(cfp))).ok()? + }; // The entry context makes no assumptions about types let blockid = BlockId { @@ -2153,10 +2159,12 @@ pub fn gen_entry_point(iseq: IseqPtr, ec: EcPtr) -> Option { let ocb = CodegenGlobals::get_outlined_cb(); // Write the interpreter entry prologue. Might be NULL when out of memory. - let code_ptr = gen_entry_prologue(cb, ocb, iseq, insn_idx); + let code_ptr = gen_entry_prologue(cb, ocb, iseq, insn_idx, jit_exception); // Try to generate code for the entry block - let block = gen_block_series(blockid, &Context::default(), ec, cb, ocb); + let mut ctx = Context::default(); + ctx.stack_size = stack_size; + let block = gen_block_series(blockid, &ctx, ec, cb, ocb); cb.mark_all_executable(); ocb.unwrap().mark_all_executable(); @@ -2239,6 +2247,9 @@ fn entry_stub_hit_body(entry_ptr: *const c_void, ec: EcPtr) -> Option<*const u8> let cfp = unsafe { get_ec_cfp(ec) }; let iseq = unsafe { get_cfp_iseq(cfp) }; let insn_idx = iseq_pc_to_insn_idx(iseq, unsafe { get_cfp_pc(cfp) })?; + let stack_size: u8 = unsafe { + u8::try_from(get_cfp_sp(cfp).offset_from(get_cfp_bp(cfp))).ok()? + }; let cb = CodegenGlobals::get_inline_cb(); let ocb = CodegenGlobals::get_outlined_cb(); @@ -2251,7 +2262,8 @@ fn entry_stub_hit_body(entry_ptr: *const c_void, ec: EcPtr) -> Option<*const u8> // Try to find an existing compiled version of this block let blockid = BlockId { iseq, idx: insn_idx }; - let ctx = Context::default(); + let mut ctx = Context::default(); + ctx.stack_size = stack_size; let blockref = match find_block_version(blockid, &ctx) { // If an existing block is found, generate a jump to the block. Some(blockref) => { diff --git a/yjit/src/cruby.rs b/yjit/src/cruby.rs index a3ce30c58902cc..254bdb1896d713 100644 --- a/yjit/src/cruby.rs +++ b/yjit/src/cruby.rs @@ -149,6 +149,7 @@ pub use rb_get_cfp_sp as get_cfp_sp; pub use rb_get_cfp_self as get_cfp_self; pub use rb_get_cfp_ep as get_cfp_ep; pub use rb_get_cfp_ep_level as get_cfp_ep_level; +pub use rb_vm_base_ptr as get_cfp_bp; pub use rb_get_cme_def_type as get_cme_def_type; pub use rb_get_cme_def_body_attr_id as get_cme_def_body_attr_id; pub use rb_get_cme_def_body_optimized_type as get_cme_def_body_optimized_type; diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 94a9335042263f..5f0601f9887aac 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -1253,7 +1253,6 @@ extern "C" { pub fn rb_get_mct_func(mct: *const rb_method_cfunc_t) -> *mut ::std::os::raw::c_void; pub fn rb_get_def_iseq_ptr(def: *mut rb_method_definition_t) -> *const rb_iseq_t; pub fn rb_get_def_bmethod_proc(def: *mut rb_method_definition_t) -> VALUE; - pub fn rb_get_iseq_body_total_calls(iseq: *const rb_iseq_t) -> ::std::os::raw::c_ulong; pub fn rb_get_iseq_body_local_iseq(iseq: *const rb_iseq_t) -> *const rb_iseq_t; pub fn rb_get_iseq_body_parent_iseq(iseq: *const rb_iseq_t) -> *const rb_iseq_t; pub fn rb_get_iseq_body_local_table_size(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; @@ -1297,6 +1296,7 @@ extern "C" { pub fn rb_get_cfp_self(cfp: *mut rb_control_frame_struct) -> VALUE; pub fn rb_get_cfp_ep(cfp: *mut rb_control_frame_struct) -> *mut VALUE; pub fn rb_get_cfp_ep_level(cfp: *mut rb_control_frame_struct, lv: u32) -> *const VALUE; + pub fn rb_vm_base_ptr(cfp: *mut rb_control_frame_struct) -> *mut VALUE; pub fn rb_yarv_class_of(obj: VALUE) -> VALUE; pub fn rb_yarv_str_eql_internal(str1: VALUE, str2: VALUE) -> VALUE; pub fn rb_str_neq_internal(str1: VALUE, str2: VALUE) -> VALUE; @@ -1340,4 +1340,5 @@ extern "C" { pub fn rb_yjit_assert_holding_vm_lock(); pub fn rb_yjit_sendish_sp_pops(ci: *const rb_callinfo) -> usize; pub fn rb_yjit_invokeblock_sp_pops(ci: *const rb_callinfo) -> usize; + pub fn rb_yjit_set_exception_return(cfp: *mut rb_control_frame_t); } diff --git a/yjit/src/yjit.rs b/yjit/src/yjit.rs index a453728702735f..7f4cbbe18d017f 100644 --- a/yjit/src/yjit.rs +++ b/yjit/src/yjit.rs @@ -47,11 +47,8 @@ pub fn yjit_enabled_p() -> bool { /// Test whether we are ready to compile an ISEQ or not #[no_mangle] -pub extern "C" fn rb_yjit_threshold_hit(iseq: IseqPtr) -> bool { - +pub extern "C" fn rb_yjit_threshold_hit(_iseq: IseqPtr, total_calls: u64) -> bool { let call_threshold = get_option!(call_threshold) as u64; - let total_calls = unsafe { rb_get_iseq_body_total_calls(iseq) } as u64; - return total_calls == call_threshold; } @@ -112,8 +109,10 @@ fn rb_bug_panic_hook() { /// Called from C code to begin compiling a function /// NOTE: this should be wrapped in RB_VM_LOCK_ENTER(), rb_vm_barrier() on the C side +/// If jit_exception is true, compile JIT code for handling exceptions. +/// See [jit_compile_exception] for details. #[no_mangle] -pub extern "C" fn rb_yjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr) -> *const u8 { +pub extern "C" fn rb_yjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exception: bool) -> *const u8 { // Reject ISEQs with very large temp stacks, // this will allow us to use u8/i8 values to track stack_size and sp_offset let stack_max = unsafe { rb_get_iseq_body_stack_max(iseq) }; @@ -131,7 +130,7 @@ pub extern "C" fn rb_yjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr) -> *con return std::ptr::null(); } - let maybe_code_ptr = gen_entry_point(iseq, ec); + let maybe_code_ptr = gen_entry_point(iseq, ec, jit_exception); match maybe_code_ptr { Some(ptr) => ptr.raw_ptr(),