diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 9db644da16f9d3..6118906823e8e0 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -98,7 +98,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@41049aa56687c35e0afa74eed4f09cec4f9afabf # v2.85.2 + - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 8e72f7d4835947..0843385bce8346 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -141,7 +141,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@41049aa56687c35e0afa74eed4f09cec4f9afabf # v2.85.2 + - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/gc.c b/gc.c index 6ab512d468b4bc..be3d82d46f13ee 100644 --- a/gc.c +++ b/gc.c @@ -1453,10 +1453,10 @@ rb_gc_obj_needs_cleanup_p(VALUE obj) return !(flags & RARRAY_EMBED_FLAG); case T_HASH: - return !(flags & RHASH_ST_TABLE_FLAG); + return (flags & RHASH_ST_TABLE_FLAG); case T_MATCH: - return !((flags & (RMATCH_ONIG | RMATCH_OFFSETS_EXTERNAL)) || USE_DEBUG_COUNTER); + return (flags & (RMATCH_ONIG | RMATCH_OFFSETS_EXTERNAL)) || USE_DEBUG_COUNTER; case T_BIGNUM: return !(flags & BIGNUM_EMBED_FLAG); diff --git a/internal/variable.h b/internal/variable.h index 461676e6fae300..53d991c2be131b 100644 --- a/internal/variable.h +++ b/internal/variable.h @@ -22,7 +22,6 @@ VALUE rb_search_class_path(VALUE); VALUE rb_attr_delete(VALUE, ID); void rb_autoload_str(VALUE mod, ID id, VALUE file); VALUE rb_autoload_at_p(VALUE, ID, int); -void rb_autoload_copy_table_for_box(st_table *, const rb_box_t *); NORETURN(VALUE rb_mod_const_missing(VALUE,VALUE)); rb_gvar_getter_t *rb_gvar_getter_function_of(ID); rb_gvar_setter_t *rb_gvar_setter_function_of(ID); diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index e15e708f7777c6..783469e56bf240 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -462,6 +462,7 @@ def fund method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)." method_option "quiet", type: :boolean, banner: "Only output warnings and errors." method_option "frozen", type: :boolean, banner: "Do not allow the Gemfile.lock to be updated after this bundle cache operation's install (removed)" + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." long_desc <<-D The cache command will copy the .gem files for every gem in the bundle into the directory ./vendor/cache. If you then check that directory into your source @@ -643,6 +644,7 @@ def inject(*) method_option "strict", type: :boolean, banner: "If updating, do not allow any gem to be updated past latest --patch | --minor | --major" method_option "conservative", type: :boolean, banner: "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated" method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler" + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def lock require_relative "cli/lock" Lock.new(options).run diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb index 2f78868936c108..f6732c59254a09 100644 --- a/lib/bundler/cli/lock.rb +++ b/lib/bundler/cli/lock.rb @@ -16,6 +16,9 @@ def run check_for_conflicting_options + Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) + Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] + print = options[:print] previous_output_stream = Bundler.ui.output_stream Bundler.ui.output_stream = :stderr if print diff --git a/lib/bundler/man/bundle-cache.1 b/lib/bundler/man/bundle-cache.1 index 38ea0479612404..81d4a9e04265d6 100644 --- a/lib/bundler/man/bundle-cache.1 +++ b/lib/bundler/man/bundle-cache.1 @@ -17,6 +17,9 @@ Include gems for all platforms present in the lockfile, not only the current one \fB\-\-cache\-path=CACHE\-PATH\fR Specify a different cache path than the default (vendor/cache)\. .TP +\fB\-\-cooldown=\fR +Only consider gem versions published at least \fInumber\fR days ago when resolving before caching\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1)\. +.TP \fB\-\-gemfile=GEMFILE\fR Use the specified gemfile instead of Gemfile\. .TP diff --git a/lib/bundler/man/bundle-cache.1.ronn b/lib/bundler/man/bundle-cache.1.ronn index 51846c96b42a25..91e16cde22844c 100644 --- a/lib/bundler/man/bundle-cache.1.ronn +++ b/lib/bundler/man/bundle-cache.1.ronn @@ -21,6 +21,12 @@ use the gems in the cache in preference to the ones on `rubygems.org`. * `--cache-path=CACHE-PATH`: Specify a different cache path than the default (vendor/cache). +* `--cooldown=`: + Only consider gem versions published at least days ago when + resolving before caching. Pass `0` to disable cooldown for this run, + overriding any per-source or global configuration. See `cooldown` in + bundle-config(1). + * `--gemfile=GEMFILE`: Use the specified gemfile instead of Gemfile. diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index c055c8a415b060..32291e048c1f90 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -139,7 +139,7 @@ Cooldown filtering depends on the gem server providing a per\-version \fBcreated .IP "\(bu" 4 \fBlockfile\fR (\fBBUNDLE_LOCKFILE\fR): The path to the lockfile that bundler should use\. By default, Bundler adds \fB\.lock\fR to the end of the \fBgemfile\fR entry\. Can be set to \fBfalse\fR in the Gemfile to disable lockfile creation entirely (see gemfile(5))\. .IP "\(bu" 4 -\fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. +\fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. Bundler's own checksum is only included when its \fB\.gem\fR file is cached, which may not be the case when Bundler is installed as a default gem\. .IP "\(bu" 4 \fBno_build_extension\fR (\fBBUNDLE_NO_BUILD_EXTENSION\fR): Whether Bundler should skip building native extensions during installation\. When set, gems are installed without compiling their C extensions\. To build extensions later, unset this setting and run \fBbundle pristine \fR\. .IP "\(bu" 4 diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 72f891b428d58b..6b52288783b5c7 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -230,6 +230,8 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). Gemfile to disable lockfile creation entirely (see gemfile(5)). * `lockfile_checksums` (`BUNDLE_LOCKFILE_CHECKSUMS`): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources. Defaults to true. + Bundler's own checksum is only included when its `.gem` file is cached, which + may not be the case when Bundler is installed as a default gem. * `no_build_extension` (`BUNDLE_NO_BUILD_EXTENSION`): Whether Bundler should skip building native extensions during installation. When set, gems are installed without compiling their C extensions. diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1 index 396c8ff6ca9766..8f6a77e435eef6 100644 --- a/lib/bundler/man/bundle-lock.1 +++ b/lib/bundler/man/bundle-lock.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-lock\fR \- Creates / Updates a lockfile without installing .SH "SYNOPSIS" -\fBbundle lock\fR [\-\-update] [\-\-bundler[=BUNDLER]] [\-\-local] [\-\-print] [\-\-lockfile=PATH] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-add\-checksums] [\-\-add\-platform] [\-\-remove\-platform] [\-\-normalize\-platforms] [\-\-patch] [\-\-minor] [\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative] +\fBbundle lock\fR [\-\-update] [\-\-bundler[=BUNDLER]] [\-\-local] [\-\-print] [\-\-lockfile=PATH] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-add\-checksums] [\-\-add\-platform] [\-\-remove\-platform] [\-\-normalize\-platforms] [\-\-patch] [\-\-minor] [\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative] [\-\-cooldown=NUMBER] .SH "DESCRIPTION" Lock the gems specified in Gemfile\. .SH "OPTIONS" @@ -59,6 +59,9 @@ If updating, do not allow any gem to be updated past latest \-\-patch | \-\-mino .TP \fB\-\-conservative\fR If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated\. +.TP +\fB\-\-cooldown=\fR +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1)\. .SH "UPDATING ALL GEMS" If you run \fBbundle lock\fR with \fB\-\-update\fR option without list of gems, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\. .SH "UPDATING A LIST OF GEMS" diff --git a/lib/bundler/man/bundle-lock.1.ronn b/lib/bundler/man/bundle-lock.1.ronn index 6d3e63c982c500..683a429b0e1ef7 100644 --- a/lib/bundler/man/bundle-lock.1.ronn +++ b/lib/bundler/man/bundle-lock.1.ronn @@ -20,6 +20,7 @@ bundle-lock(1) -- Creates / Updates a lockfile without installing [--pre] [--strict] [--conservative] + [--cooldown=NUMBER] ## DESCRIPTION @@ -84,6 +85,11 @@ Lock the gems specified in Gemfile. * `--conservative`: If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated. +* `--cooldown=`: + Only consider gem versions published at least days ago when + resolving. Pass `0` to disable cooldown for this run, overriding any + per-source or global configuration. See `cooldown` in bundle-config(1). + ## UPDATING ALL GEMS If you run `bundle lock` with `--update` option without list of gems, bundler will diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index 8d42f4ad8255eb..2724132263cd84 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -115,6 +115,19 @@ build_gem "fresh_gem", "0.3.2" do |s| s.date = now - (1 * 86_400) end + + # the generic build is outside the window, but a platform-specific + # build of the same version was pushed inside it + build_gem "late_platform", "1.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "late_platform", "2.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "late_platform", "2.0.0" do |s| + s.platform = "x86_64-linux" + s.date = now - (1 * 86_400) + end end end @@ -712,6 +725,85 @@ expect(lockfile).not_to include("ripe_gem (2.0.0)") end + it "excludes a version on every platform when a platform-specific build of it is inside the window" do + # Exclusion is keyed on [name, version] and deliberately ignores + # platform: otherwise pushing a fresh platform-specific build under an + # already-ripe version number would slip new code past the cooldown. + gemfile <<-G + source "https://gem.repo3" + gem "late_platform" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("late_platform 1.0.0") + end + + it "selects the version with a late platform-specific build when --cooldown 0 bypasses the filter" do + gemfile <<-G + source "https://gem.repo3" + gem "late_platform" + G + + bundle "install --cooldown 0", artifice: "compact_index_cooldown" + + # On x86_64-linux hosts this resolves to the platform-specific build, so + # assert on the lockfile instead of the installed platform. + expect(lockfile).to include("late_platform (2.0.0") + expect(lockfile).not_to include("late_platform (1.0.0)") + end + + it "applies CLI --cooldown on bundle lock --update" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update --cooldown 7", artifice: "compact_index_cooldown" + + expect(lockfile).to include("ripe_gem (1.0.0)") + expect(lockfile).not_to include("ripe_gem (2.0.0)") + end + + it "rejects a negative --cooldown value on bundle lock" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "lock --cooldown=-7", artifice: "compact_index_cooldown", raise_on_error: false + + expect(err).to match(/non-negative integer/) + end + + it "applies CLI --cooldown on bundle cache" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "cache --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + expect(bundled_app("vendor/cache/ripe_gem-1.0.0.gem")).to exist + end + it "ignores cooldown and installs the locked version when frozen" do # Frozen installs read the lockfile instead of resolving, so cooldown has # no say. A version already locked inside the window must still install. diff --git a/test/openssl/test_pkcs7.rb b/test/openssl/test_pkcs7.rb index b3129c0cdfe610..c677d8223bdb21 100644 --- a/test/openssl/test_pkcs7.rb +++ b/test/openssl/test_pkcs7.rb @@ -238,8 +238,6 @@ def test_enveloped assert_equal(@ee2_cert.serial, recip[1].serial) assert_equal(data, p7.decrypt(@ee2_key, @ee2_cert)) - assert_equal(data, p7.decrypt(@ee1_key)) - assert_raise(OpenSSL::PKCS7::PKCS7Error) { p7.decrypt(@ca_key, @ca_cert) } @@ -250,6 +248,16 @@ def test_enveloped } end + def test_enveloped_decrypt_key_only + omit_on_fips # PKCS #1 v1.5 padding + + data = "aaaaa\nbbbbb\nccccc\n" + cipher = "AES-128-CBC" + + p7 = OpenSSL::PKCS7.encrypt([@ee1_cert], data, cipher, OpenSSL::PKCS7::BINARY) + assert_equal(data, p7.decrypt(@ee1_key)) + end + def test_enveloped_add_recipient omit_on_fips # PKCS #1 v1.5 padding diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index 2b844f4ceff8ac..c60d973a994115 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -419,6 +419,21 @@ def test_detailed_message_in_ractor RUBY end + def test_ractor_vm_once_dispatch + assert_ractor(<<~'RUBY', args: ["-W0"], timeout: 30) + vals = 10.times.map do + Ractor.new { + a = nil + /#{sleep 0.1; a = "set"}/o + a + } + end.map(&:value) + vals.compact! + assert_equal 1, vals.size + assert_equal "set", vals.first + RUBY + end + def assert_make_shareable(obj) refute Ractor.shareable?(obj), "object was already shareable" Ractor.make_shareable(obj) diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index 9f509cc4f220c1..42eb44f4a6122f 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -1016,11 +1016,10 @@ def test_thread_timer_and_interrupt cmd = 'Signal.trap(:INT, "DEFAULT"); pipe=IO.pipe; Thread.start {Thread.pass until Thread.main.stop?; puts; STDOUT.flush}; pipe[0].read' opt = {} opt[:new_pgroup] = true if /mswin|mingw/ =~ RUBY_PLATFORM - s, t, _err = EnvUtil.invoke_ruby(['-e', cmd], "", true, true, **opt) do |in_p, out_p, err_p, cpid| + s, err = EnvUtil.invoke_ruby(['-e', cmd], "", true, true, **opt) do |in_p, out_p, err_p, cpid| assert IO.select([out_p], nil, nil, 10), 'subprocess not ready' out_p.gets pid = cpid - t0 = Time.now.to_f Process.kill(:SIGINT, pid) begin Timeout.timeout(10) { Process.wait(pid) } @@ -1028,14 +1027,12 @@ def test_thread_timer_and_interrupt EnvUtil.terminate(pid) raise end - t1 = Time.now.to_f - [$?, t1 - t0, err_p.read] + [$?, err_p.read] end assert_equal(pid, s.pid, bug5757) assert_equal([false, true, false, Signal.list["INT"]], [s.exited?, s.signaled?, s.stopped?, s.termsig], "[s.exited?, s.signaled?, s.stopped?, s.termsig]") - assert_include(0..2, t, bug5757) end def test_thread_join_in_trap diff --git a/thread.c b/thread.c index 3b32ddb2cf8a6c..e07e1cda3a57e7 100644 --- a/thread.c +++ b/thread.c @@ -5083,6 +5083,8 @@ rb_thread_atfork_internal(rb_thread_t *th, void (*atfork)(rb_thread_t *, const r /* may be held by any thread in parent */ rb_native_mutex_initialize(&th->interrupt_lock); + rb_native_mutex_initialize(&vm->once_lock); + rb_native_cond_initialize(&vm->once_cond); ccan_list_head_init(&th->interrupt_exec_tasks); vm->fork_gen++; @@ -5755,6 +5757,8 @@ Init_Thread_Mutex(void) rb_thread_t *th = GET_THREAD(); rb_native_mutex_initialize(&th->vm->workqueue_lock); + rb_native_mutex_initialize(&th->vm->once_lock); + rb_native_cond_initialize(&th->vm->once_cond); rb_native_mutex_initialize(&th->interrupt_lock); } diff --git a/variable.c b/variable.c index e0b4d4a45db2a7..a0f24c0868dd55 100644 --- a/variable.c +++ b/variable.c @@ -2687,70 +2687,6 @@ get_autoload_data(VALUE autoload_const_value, struct autoload_const **autoload_c return autoload_data; } -struct autoload_copy_table_data { - VALUE dst_tbl_value; - struct st_table *dst_tbl; - const rb_box_t *box; -}; - -static int -autoload_copy_table_for_box_i(st_data_t key, st_data_t value, st_data_t arg) -{ - struct autoload_const *autoload_const; - struct autoload_copy_table_data *data = (struct autoload_copy_table_data *)arg; - struct st_table *tbl = data->dst_tbl; - VALUE tbl_value = data->dst_tbl_value; - const rb_box_t *box = data->box; - - VALUE src_value = (VALUE)value; - struct autoload_const *src_const = rb_check_typeddata(src_value, &autoload_const_type); - // autoload_data can be shared between copies because the feature is equal between copies. - VALUE autoload_data_value = src_const->autoload_data_value; - struct autoload_data *autoload_data = rb_check_typeddata(autoload_data_value, &autoload_data_type); - - VALUE new_value = TypedData_Make_Struct(0, struct autoload_const, &autoload_const_type, autoload_const); - RB_OBJ_WRITE(new_value, &autoload_const->box_value, rb_get_box_object((rb_box_t *)box)); - RB_OBJ_WRITE(new_value, &autoload_const->module, src_const->module); - autoload_const->name = src_const->name; - RB_OBJ_WRITE(new_value, &autoload_const->value, src_const->value); - autoload_const->flag = src_const->flag; - RB_OBJ_WRITE(new_value, &autoload_const->autoload_data_value, autoload_data_value); - ccan_list_add_tail(&autoload_data->constants, &autoload_const->cnode); - - st_insert(tbl, (st_data_t)autoload_const->name, (st_data_t)new_value); - RB_OBJ_WRITTEN(tbl_value, Qundef, new_value); - - return ST_CONTINUE; -} - -void -rb_autoload_copy_table_for_box(st_table *iv_ptr, const rb_box_t *box) -{ - struct st_table *src_tbl, *dst_tbl; - VALUE src_tbl_value, dst_tbl_value; - if (!rb_st_lookup(iv_ptr, (st_data_t)autoload, (st_data_t *)&src_tbl_value)) { - // the class has no autoload table yet. - return; - } - if (!RTEST(src_tbl_value) || !(src_tbl = check_autoload_table(src_tbl_value))) { - // the __autoload__ ivar value isn't autoload table value. - return; - } - src_tbl = check_autoload_table(src_tbl_value); - - dst_tbl_value = TypedData_Wrap_Struct(0, &autoload_table_type, NULL); - RTYPEDDATA_DATA(dst_tbl_value) = dst_tbl = st_init_numtable(); - - struct autoload_copy_table_data data = { - .dst_tbl_value = dst_tbl_value, - .dst_tbl = dst_tbl, - .box = box, - }; - - st_foreach(src_tbl, autoload_copy_table_for_box_i, (st_data_t)&data); - st_insert(iv_ptr, (st_data_t)autoload, (st_data_t)dst_tbl_value); -} - void rb_autoload(VALUE module, ID name, const char *feature) { diff --git a/vm.c b/vm.c index 213b396bc09e50..d95fab6b0810db 100644 --- a/vm.c +++ b/vm.c @@ -3566,6 +3566,8 @@ ruby_vm_destruct(rb_vm_t *vm) rb_objspace_free(objspace); } rb_native_mutex_destroy(&vm->workqueue_lock); + rb_native_mutex_destroy(&vm->once_lock); + rb_native_cond_destroy(&vm->once_cond); /* after freeing objspace, you *can't* use ruby_xfree() */ ruby_current_vm_ptr = NULL; diff --git a/vm_core.h b/vm_core.h index 0ca0598678edc2..73546f7642e564 100644 --- a/vm_core.h +++ b/vm_core.h @@ -802,6 +802,10 @@ typedef struct rb_vm_struct { struct ccan_list_head workqueue; /* <=> rb_workqueue_job.jnode */ rb_nativethread_lock_t workqueue_lock; + /* `once` completion event (see vm_once_dispatch) */ + rb_nativethread_lock_t once_lock; + rb_nativethread_cond_t once_cond; + VALUE orig_progname, progname; VALUE coverages, me2counter; int coverage_mode; diff --git a/vm_insnhelper.c b/vm_insnhelper.c index e7acfd8a795770..ce27c435c3c67e 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -28,6 +28,7 @@ #include "internal/variable.h" #include "internal/set_table.h" #include "internal/struct.h" +#include "ruby/thread.h" #include "variable.h" /* finish iseq array */ @@ -5499,11 +5500,49 @@ vm_once_exec(VALUE iseq) return rb_proc_call_with_block(proc, 0, 0, Qnil); } +#define RUNNING_THREAD_ONCE_DONE ((rb_thread_t *)0x1) + +struct vm_once_wait_arg { + rb_vm_t *vm; + union iseq_inline_storage_entry *is; +}; + +static void +vm_once_broadcast(rb_vm_t *vm) +{ + rb_native_mutex_lock(&vm->once_lock); + rb_native_cond_broadcast(&vm->once_cond); + rb_native_mutex_unlock(&vm->once_lock); +} + +static void * +vm_once_wait_no_gvl(void *ptr) +{ + struct vm_once_wait_arg *arg = (struct vm_once_wait_arg *)ptr; + rb_vm_t *vm = arg->vm; + rb_thread_t *running_th; + + rb_native_mutex_lock(&vm->once_lock); + running_th = rbimpl_atomic_ptr_load((void **)&arg->is->once.running_thread, RBIMPL_ATOMIC_ACQUIRE); + if (running_th != NULL && running_th != RUNNING_THREAD_ONCE_DONE) { + rb_native_cond_wait(&vm->once_cond, &vm->once_lock); + } + rb_native_mutex_unlock(&vm->once_lock); + return NULL; +} + +static void +vm_once_wait_ubf(void *ptr) +{ + vm_once_broadcast((rb_vm_t *)ptr); +} + static VALUE vm_once_clear(VALUE data) { union iseq_inline_storage_entry *is = (union iseq_inline_storage_entry *)data; - is->once.running_thread = NULL; + rbimpl_atomic_ptr_store((volatile void **)&is->once.running_thread, NULL, RBIMPL_ATOMIC_RELEASE); + vm_once_broadcast(GET_VM()); return Qnil; } @@ -6605,36 +6644,50 @@ static VALUE vm_once_dispatch(rb_execution_context_t *ec, ISEQ iseq, ISE is) { rb_thread_t *th = rb_ec_thread_ptr(ec); - rb_thread_t *const RUNNING_THREAD_ONCE_DONE = (rb_thread_t *)(0x1); + rb_thread_t *running_th; again: - if (is->once.running_thread == RUNNING_THREAD_ONCE_DONE) { + running_th = rbimpl_atomic_ptr_load((void**)&is->once.running_thread, RBIMPL_ATOMIC_ACQUIRE); + if (running_th == RUNNING_THREAD_ONCE_DONE) { return is->once.value; } - else if (is->once.running_thread == NULL) { - VALUE val; - is->once.running_thread = th; - val = rb_ensure(vm_once_exec, (VALUE)iseq, vm_once_clear, (VALUE)is); - // TODO: confirm that it is shareable + else if (running_th == NULL) { + VALUE val = Qundef; + enum ruby_tag_type state; + if (rbimpl_atomic_ptr_cas((void**)&is->once.running_thread, running_th, th, RBIMPL_ATOMIC_RELEASE, RBIMPL_ATOMIC_RELAXED) != running_th) { + goto again; + } + EC_PUSH_TAG(ec); + if ((state = EC_EXEC_TAG()) == TAG_NONE) { + val = vm_once_exec((VALUE)iseq); + } + EC_POP_TAG(); + if (state != TAG_NONE) { + vm_once_clear((VALUE)is); + EC_JUMP_TAG(ec, state); + } + // TODO: confirm that it is shareable if (RB_FL_ABLE(val)) { RB_OBJ_SET_SHAREABLE(val); } RB_OBJ_WRITE(CFP_ISEQ(ec->cfp), &is->once.value, val); - /* is->once.running_thread is cleared by vm_once_clear() */ - is->once.running_thread = RUNNING_THREAD_ONCE_DONE; /* success */ + rbimpl_atomic_ptr_store((volatile void**)&is->once.running_thread, RUNNING_THREAD_ONCE_DONE, RBIMPL_ATOMIC_RELEASE); /* success */ + vm_once_broadcast(rb_ec_vm_ptr(ec)); return val; } - else if (is->once.running_thread == th) { + else if (running_th == th) { /* recursive once */ return vm_once_exec((VALUE)iseq); } else { - /* waiting for finish */ + /* wait for the winner to finish */ + struct vm_once_wait_arg arg = { .vm = rb_ec_vm_ptr(ec), .is = is }; + rb_nogvl(vm_once_wait_no_gvl, &arg, vm_once_wait_ubf, arg.vm, + RB_NOGVL_UBF_ASYNC_SAFE | RB_NOGVL_INTR_FAIL); RUBY_VM_CHECK_INTS(ec); - rb_thread_schedule(); goto again; } } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index da6283fb0b5ee8..51b488b9e8a51f 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -213,7 +213,7 @@ pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exc fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool) -> Result { // We don't support exception handlers yet if jit_exception { - return Err(CompileError::ExceptionHandler); + return gen_exception_handler_counter(cb); } let iseq_name = iseq_get_location(iseq, 0); @@ -525,7 +525,6 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func debug!("ZJIT: gen_function: Failed to compile insn: {insn_id} {insn}. Generating side-exit."); gen_incr_counter(&mut asm, exit_counter_for_unhandled_hir_insn(&insn)); let reason = match insn { - Insn::Throw { .. } => SideExitReason::UnhandledHIRThrow, Insn::InvokeBuiltin { .. } => SideExitReason::UnhandledHIRInvokeBuiltin, _ => SideExitReason::UnhandledHIRUnknown(insn_id), }; @@ -784,7 +783,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::IsA { val, class } => gen_is_a(jit, asm, opnd!(val), opnd!(class)), &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 { state, .. } => return Err(state), + &Insn::Throw { state, .. } => no_output!(gen_throw(jit, asm, function, &function.frame_state(state))), &Insn::CondBranch { .. } | &Insn::Jump { .. } | Insn::Entries { .. } => unreachable!(), }; @@ -2652,6 +2651,12 @@ fn gen_return(asm: &mut Assembler, val: lir::Opnd) { asm.cret(C_RET_OPND); } +fn gen_throw(jit: &mut JITState, asm: &mut Assembler, function: &Function, state: &FrameState) { + // TODO: Consider calling rb_vm_throw and propagating ec->tag->state to the interpreter. + // Also consider making it a jump on method inlining. + gen_side_exit(jit, asm, function, &SideExitReason::Throw, None, state); +} + /// Compile Fixnum + Fixnum fn gen_fixnum_add(jit: &mut JITState, asm: &mut Assembler, function: &Function, left: lir::Opnd, right: lir::Opnd, state: &FrameState) -> lir::Opnd { // Add left + right and test for overflow @@ -4041,6 +4046,19 @@ fn gen_compile_error_counter(cb: &mut CodeBlock, compile_error: &CompileError) - }) } +/// Generate a JIT entry that just increments exit_exception_handler and exits +fn gen_exception_handler_counter(cb: &mut CodeBlock) -> Result { + let mut asm = Assembler::new(); + asm.new_block_without_id("exception_handler_counter"); + gen_incr_counter(&mut asm, Counter::exit_exception_handler); + asm.cret(Qundef.into()); + + asm.compile(cb).map(|(code_ptr, gc_offsets)| { + assert_eq!(0, gc_offsets.len()); + code_ptr + }) +} + /// Given the number of spill slots needed for a function, return the number of bytes /// the function needs to allocate on the stack for the stack frame. fn aligned_stack_bytes(num_slots: usize) -> usize { diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index d03bd2d65e52e3..a943f06d572400 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -6012,6 +6012,18 @@ fn test_invokeblock_ifunc_map() { assert_snapshot!(assert_compiles("test"), @"[2, 4, 6]"); } +#[test] +fn test_invokeblock_ifunc_kwarg() { + eval(" + def foo + yield 1, a: 2 + end + def test = enum_for(:foo).to_a + test + "); + assert_snapshot!(assert_compiles("test"), @"[[1, {a: 2}]]"); +} + #[test] fn test_ccall_variadic_with_multiple_args() { eval(" diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 21c1c701cf0897..6d3fa90f10599e 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -553,6 +553,7 @@ pub enum SideExitReason { PatchPoint(Invariant), CalleeSideExit, Interrupt, + Throw, BlockParamProxyNotIseqOrIfunc, BlockParamProxyNotNil, BlockParamProxyNotProc, @@ -9325,7 +9326,7 @@ fn add_iseq_to_hir( Some(summary.bucket(0).class()) }); - let is_ifunc = (flags & (VM_CALL_ARGS_SPLAT | VM_CALL_KW_SPLAT)) == 0 + let is_ifunc = (flags & (VM_CALL_ARGS_SPLAT | VM_CALL_KW_SPLAT | VM_CALL_KWARG)) == 0 && block_handler_class.is_some_and(|obj| unsafe { rb_IMEMO_TYPE_P(obj, imemo_ifunc) == 1 }); // If the block handler is a known simple ISEQ block with exact arity and no diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 1682fe23eca4bb..4d50dc51772d97 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -17913,6 +17913,34 @@ mod hir_opt_tests { "); } + #[test] + fn test_invokeblock_ifunc_kwarg() { + eval(" + def foo + yield 1, a: 2 + end + def test = enum_for(:foo).to_a + test + "); + assert_snapshot!(hir_string("foo"), @" + fn foo@: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:Fixnum[1] = Const Value(1) + v12:Fixnum[2] = Const Value(2) + v14:BasicObject = InvokeBlock v10, v12 # SendFallbackReason: InvokeBlock: not yet specialized + CheckInterrupts + Return v14 + "); + } + #[test] fn test_dedup_guard_type() { // Two subtractions on the same Fixnum operand `n` each require a diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 8a0c55c8d58667..7ebfb62309a0d9 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -185,6 +185,7 @@ make_counters! { exit { // exit_: Side exits reasons exit_compile_error, + exit_exception_handler, exit_unhandled_newarray_send_min, exit_unhandled_newarray_send_hash, exit_unhandled_newarray_send_pack, @@ -226,6 +227,7 @@ make_counters! { exit_patchpoint_root_box_only, exit_callee_side_exit, exit_interrupt, + exit_throw, exit_stackoverflow, exit_block_param_proxy_not_iseq_or_ifunc, exit_block_param_proxy_not_nil, @@ -349,7 +351,6 @@ make_counters! { compile_error_iseq_version_limit_reached, compile_error_iseq_stack_too_large, compile_error_native_stack_too_large, - compile_error_exception_handler, compile_error_out_of_memory, compile_error_label_linking_failure, compile_error_jit_to_jit_optional, @@ -369,9 +370,6 @@ make_counters! { compile_error_validation_misc_validation_error, // unhandled_hir_insn_: Unhandled HIR instructions - unhandled_hir_insn_array_max, - unhandled_hir_insn_fixnum_div, - unhandled_hir_insn_throw, unhandled_hir_insn_invokebuiltin, unhandled_hir_insn_unknown, @@ -525,7 +523,6 @@ pub enum CompileError { IseqVersionLimitReached, IseqStackTooLarge, NativeStackTooLarge, - ExceptionHandler, OutOfMemory, ParseError(ParseError), /// When a ZJIT function is too large, the branches may have @@ -544,7 +541,6 @@ pub fn exit_counter_for_compile_error(compile_error: &CompileError) -> Counter { IseqVersionLimitReached => compile_error_iseq_version_limit_reached, IseqStackTooLarge => compile_error_iseq_stack_too_large, NativeStackTooLarge => compile_error_native_stack_too_large, - ExceptionHandler => compile_error_exception_handler, OutOfMemory => compile_error_out_of_memory, LabelLinkingFailure => compile_error_label_linking_failure, ParseError(parse_error) => match parse_error { @@ -570,9 +566,6 @@ pub fn exit_counter_for_unhandled_hir_insn(insn: &crate::hir::Insn) -> Counter { use crate::hir::Insn::*; use crate::stats::Counter::*; match insn { - ArrayMax { .. } => unhandled_hir_insn_array_max, - FixnumDiv { .. } => unhandled_hir_insn_fixnum_div, - Throw { .. } => unhandled_hir_insn_throw, InvokeBuiltin { .. } => unhandled_hir_insn_invokebuiltin, _ => unhandled_hir_insn_unknown, } @@ -619,6 +612,7 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { GuardSuperMethodEntry => exit_guard_super_method_entry, CalleeSideExit => exit_callee_side_exit, Interrupt => exit_interrupt, + Throw => exit_throw, StackOverflow => exit_stackoverflow, BlockParamProxyNotIseqOrIfunc => exit_block_param_proxy_not_iseq_or_ifunc, BlockParamProxyNotNil => exit_block_param_proxy_not_nil,