diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index e263f34f09ac66..3fb76118aea3c4 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -1534,10 +1534,10 @@ class C } assert_equal '1', %q{ - N = 1_000 + N = 100 Ractor.new{ a = [] - 1_000.times.map{|i| + 100.times.map{|i| Thread.new(i){|i| Thread.pass if i < N a << Ractor.store_if_absent(:i){ i } diff --git a/gc/default/default.c b/gc/default/default.c index 41417f13f377b2..139fb142c0b24b 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -8586,10 +8586,32 @@ gc_enter_count(enum gc_enter_event event) static bool current_process_time(struct timespec *ts); +/* A gc phase must be timed on the collecting thread's own cpu. A local gc runs + * while the other ractors keep going, and process cpu time counts their work as + * gc: with eight busy ractors the same ten collections were reported as 131ms + * instead of 3ms, more than the wall clock they ran in. The kernel also answers + * this one without walking every thread in the process. */ +static bool +current_thread_time(struct timespec *ts) +{ +#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_THREAD_CPUTIME_ID) + { + static int try_clock_gettime = 1; + if (try_clock_gettime && clock_gettime(CLOCK_THREAD_CPUTIME_ID, ts) == 0) { + return true; + } + else { + try_clock_gettime = 0; + } + } +#endif + return current_process_time(ts); +} + static void gc_clock_start(struct timespec *ts) { - if (!current_process_time(ts)) { + if (!current_thread_time(ts)) { ts->tv_sec = 0; ts->tv_nsec = 0; } @@ -8601,7 +8623,7 @@ gc_clock_end(struct timespec *ts) struct timespec end_time; if ((ts->tv_sec > 0 || ts->tv_nsec > 0) && - current_process_time(&end_time) && + current_thread_time(&end_time) && end_time.tv_sec >= ts->tv_sec) { return (unsigned long long)(end_time.tv_sec - ts->tv_sec) * (1000 * 1000 * 1000) + (end_time.tv_nsec - ts->tv_nsec); diff --git a/internal/struct.h b/internal/struct.h index fc1041bf1852da..c081bdba377ac5 100644 --- a/internal/struct.h +++ b/internal/struct.h @@ -18,7 +18,7 @@ * header, rather than being on a separately allocated buffer) and * these bits are the length of the Struct. */ -enum { +enum ruby_rstruct_flags { RSTRUCT_EMBED_LEN_MASK = RUBY_FL_USER7 | RUBY_FL_USER6 | RUBY_FL_USER5 | RUBY_FL_USER4 | RUBY_FL_USER3 | RUBY_FL_USER2 | RUBY_FL_USER1, RSTRUCT_EMBED_LEN_SHIFT = (RUBY_FL_USHIFT+1), diff --git a/jit.c b/jit.c index e0866817f35c45..7f6f161f2f4cdf 100644 --- a/jit.c +++ b/jit.c @@ -20,6 +20,7 @@ #include "internal/string.h" #include "internal/class.h" #include "internal/imemo.h" +#include "internal/struct.h" #include "ruby/internal/core/rtypeddata.h" #include "zjit.h" diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 9655a8ad1b7de1..d7c61b3066e69e 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -342,6 +342,7 @@ def update(*gems) D method_option "paths", type: :boolean, banner: "List the paths of all gems that are required by your Gemfile." method_option "outdated", type: :boolean, banner: "Show verbose output including whether gems are outdated (removed)." + method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name" def show(gem_name = nil) if ARGV.include?("--outdated") removed_message = "the `--outdated` flag to `bundle show` has been removed in favor of `bundle show --verbose`" @@ -367,6 +368,7 @@ def list desc "info GEM [OPTIONS]", "Show information for the given gem" method_option "path", type: :boolean, banner: "Print full path to gem" method_option "version", type: :boolean, banner: "Print gem version" + method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name" def info(gem_name) require_relative "cli/info" Info.new(options, gem_name).run @@ -530,6 +532,7 @@ def exec(*args) desc "open GEM", "Opens the source directory of the given bundled gem" method_option "path", type: :string, lazy_default: "", banner: "Open relative path of the gem source." + method_option "exact-match", type: :boolean, banner: "Only match gems whose names exactly match the given name" def open(name) require_relative "cli/open" Open.new(options, name).run diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb index 1f66a113c2abdc..4e5d85e903b2df 100644 --- a/lib/bundler/cli/common.rb +++ b/lib/bundler/cli/common.rb @@ -122,6 +122,10 @@ def self.select_spec(name, regex_match = nil) raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) end + def self.select_spec_with_match_type(name, options) + select_spec(name, options["exact-match"] ? nil : :regex_match) + end + def self.default_gem_spec(name) gem_spec = Gem::Specification.find_all_by_name(name).last gem_spec if gem_spec&.default_gem? diff --git a/lib/bundler/cli/info.rb b/lib/bundler/cli/info.rb index cd01d4949bc98b..a0dd5a54876a1c 100644 --- a/lib/bundler/cli/info.rb +++ b/lib/bundler/cli/info.rb @@ -26,7 +26,7 @@ def run private def spec_for_gem(name) - Bundler::CLI::Common.select_spec(name, :regex_match) + Bundler::CLI::Common.select_spec_with_match_type(name, options) end def print_gem_version(spec) diff --git a/lib/bundler/cli/open.rb b/lib/bundler/cli/open.rb index 64d44e6a0b7c0e..f6aeecf8139a0f 100644 --- a/lib/bundler/cli/open.rb +++ b/lib/bundler/cli/open.rb @@ -13,7 +13,7 @@ def run raise InvalidOption, "Cannot specify `--path` option without a value" if !@path.nil? && @path.empty? editor = [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? } return Bundler.ui.info("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") unless editor - return unless spec = Bundler::CLI::Common.select_spec(name, :regex_match) + return unless spec = spec_for_gem(name) if spec.default_gem? Bundler.ui.info "Unable to open #{name} because it's a default gem, so the directory it would normally be installed to does not exist." else @@ -35,5 +35,11 @@ def editor_command(editor) require "shellwords" Shellwords.split(editor) end + + private + + def spec_for_gem(name) + Bundler::CLI::Common.select_spec_with_match_type(name, options) + end end end diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb index 67fdcc797e8592..6e88bd476515da 100644 --- a/lib/bundler/cli/show.rb +++ b/lib/bundler/cli/show.rb @@ -20,7 +20,7 @@ def run if gem_name == "bundler" path = File.expand_path("../../..", __dir__) else - spec = Bundler::CLI::Common.select_spec(gem_name, :regex_match) + spec = spec_for_gem(gem_name) return unless spec path = spec.full_gem_path unless File.directory?(path) @@ -55,6 +55,10 @@ def run private + def spec_for_gem(name) + Bundler::CLI::Common.select_spec_with_match_type(name, options) + end + def fetch_latest_specs definition = Bundler.definition(true) Bundler.ui.info "Fetching remote specs for outdated check...\n\n" diff --git a/lib/bundler/man/bundle-info.1 b/lib/bundler/man/bundle-info.1 index 49c2295f8c981f..85bd2dd0c011a6 100644 --- a/lib/bundler/man/bundle-info.1 +++ b/lib/bundler/man/bundle-info.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-info\fR \- Show information for the given gem in your bundle .SH "SYNOPSIS" -\fBbundle info\fR [GEM_NAME] [\-\-path] [\-\-version] +\fBbundle info\fR [GEM_NAME] [\-\-path] [\-\-version] [\-\-exact\-match] .SH "DESCRIPTION" Given a gem name present in your bundle, print the basic information about it such as homepage, version, path and summary\. .SH "OPTIONS" @@ -14,4 +14,7 @@ Print the path of the given gem .TP \fB\-\-version\fR Print gem version +.TP +\fB\-\-exact\-match\fR +Only match gems whose names exactly match the given name\. diff --git a/lib/bundler/man/bundle-info.1.ronn b/lib/bundler/man/bundle-info.1.ronn index e99db8c6149c8d..38cd19f4312cb6 100644 --- a/lib/bundler/man/bundle-info.1.ronn +++ b/lib/bundler/man/bundle-info.1.ronn @@ -6,6 +6,7 @@ bundle-info(1) -- Show information for the given gem in your bundle `bundle info` [GEM_NAME] [--path] [--version] + [--exact-match] ## DESCRIPTION @@ -19,3 +20,6 @@ Given a gem name present in your bundle, print the basic information about it * `--version`: Print gem version + +* `--exact-match`: + Only match gems whose names exactly match the given name. diff --git a/lib/bundler/man/bundle-open.1 b/lib/bundler/man/bundle-open.1 index 2aab59f14b9b16..1ecb3e4c77a747 100644 --- a/lib/bundler/man/bundle-open.1 +++ b/lib/bundler/man/bundle-open.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-open\fR \- Opens the source directory for a gem in your bundle .SH "SYNOPSIS" -\fBbundle open\fR [GEM] [\-\-path=PATH] +\fBbundle open\fR [GEM] [\-\-path=PATH] [\-\-exact\-match] .SH "DESCRIPTION" Opens the source directory of the provided GEM in your editor\. .P @@ -29,4 +29,7 @@ Will open the README\.md file of the 'rack' gem source in your bundle\. .TP \fB\-\-path[=PATH]\fR Specify GEM source relative path to open\. +.TP +\fB\-\-exact\-match\fR +Only match gems whose names exactly match the given name\. diff --git a/lib/bundler/man/bundle-open.1.ronn b/lib/bundler/man/bundle-open.1.ronn index 24dbe97e44563a..c345b88adc586a 100644 --- a/lib/bundler/man/bundle-open.1.ronn +++ b/lib/bundler/man/bundle-open.1.ronn @@ -3,7 +3,7 @@ bundle-open(1) -- Opens the source directory for a gem in your bundle ## SYNOPSIS -`bundle open` [GEM] [--path=PATH] +`bundle open` [GEM] [--path=PATH] [--exact-match] ## DESCRIPTION @@ -26,3 +26,6 @@ Will open the README.md file of the 'rack' gem source in your bundle. * `--path[=PATH]`: Specify GEM source relative path to open. + +* `--exact-match`: + Only match gems whose names exactly match the given name. diff --git a/lib/bundler/man/bundle-show.1 b/lib/bundler/man/bundle-show.1 index a2142694b8d317..3afbc2c06750d2 100644 --- a/lib/bundler/man/bundle-show.1 +++ b/lib/bundler/man/bundle-show.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem .SH "SYNOPSIS" -\fBbundle show\fR [GEM] [\-\-paths] +\fBbundle show\fR [GEM] [\-\-paths] [\-\-exact\-match] .SH "DESCRIPTION" Without the [GEM] option, \fBshow\fR will print a list of the names and versions of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by name\. .P @@ -13,4 +13,7 @@ Calling show with [GEM] will list the exact location of that gem on your machine .TP \fB\-\-paths\fR List the paths of all gems that are required by your [\fBGemfile(5)\fR][Gemfile(5)], sorted by gem name\. +.TP +\fB\-\-exact\-match\fR +Only match gems whose names exactly match the given name\. diff --git a/lib/bundler/man/bundle-show.1.ronn b/lib/bundler/man/bundle-show.1.ronn index a6a59a1445dcd3..2f679e5f7d1f82 100644 --- a/lib/bundler/man/bundle-show.1.ronn +++ b/lib/bundler/man/bundle-show.1.ronn @@ -5,6 +5,7 @@ bundle-show(1) -- Shows all the gems in your bundle, or the path to a gem `bundle show` [GEM] [--paths] + [--exact-match] ## DESCRIPTION @@ -19,3 +20,6 @@ machine. * `--paths`: List the paths of all gems that are required by your [`Gemfile(5)`][Gemfile(5)], sorted by gem name. + +* `--exact-match`: + Only match gems whose names exactly match the given name. diff --git a/spec/bundler/commands/info_spec.rb b/spec/bundler/commands/info_spec.rb index a26b1696fbdf99..317a88863c9eb0 100644 --- a/spec/bundler/commands/info_spec.rb +++ b/spec/bundler/commands/info_spec.rb @@ -207,7 +207,31 @@ end context "with a valid regexp for gem name" do - it "presents alternatives", :readline do + it "returns the exact match without prompting when requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "info myrack --exact-match" + expect(out).to include("* myrack (1.0.0)") + expect(out).not_to include("0 : - exit -") + end + + it "does not fall back to regexp matching when exact matching is requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "info rac --exact-match", raise_on_error: false + expect(err).to include("Could not find gem 'rac'.") + expect(out).not_to include("0 : - exit -") + end + + it "presents alternatives without the exact match flag", :readline do install_gemfile <<-G source "https://gem.repo1" gem "myrack" @@ -215,7 +239,9 @@ G bundle "info rac" - expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/) + expect(out).to include("1 : myrack") + expect(out).to include("2 : myrack-obama") + expect(out).to include("0 : - exit -") end end diff --git a/spec/bundler/commands/open_spec.rb b/spec/bundler/commands/open_spec.rb index 664dc58919cf62..085105b8c19bcc 100644 --- a/spec/bundler/commands/open_spec.rb +++ b/spec/bundler/commands/open_spec.rb @@ -172,4 +172,33 @@ expect(out).to include("Unable to open json because it's a default gem, so the directory it would normally be installed to does not exist.") end end + + context "with a valid regexp for gem name" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + end + + it "returns the exact match without prompting when requested" do + bundle "open myrack --exact-match", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("editor #{default_bundle_path("gems", "myrack-1.0.0")}") + expect(out).not_to include("0 : - exit -") + end + + it "does not fall back to regexp matching when exact matching is requested" do + bundle "open rac --exact-match", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false + expect(err).to include("Could not find gem 'rac'.") + expect(out).not_to include("0 : - exit -") + end + + it "presents alternatives without the exact match flag", :readline do + bundle "open rac", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" } + expect(out).to include("1 : myrack") + expect(out).to include("2 : myrack-obama") + expect(out).to include("0 : - exit -") + end + end end diff --git a/spec/bundler/commands/show_spec.rb b/spec/bundler/commands/show_spec.rb index b2475f91e37edf..75d46587e4ca4c 100644 --- a/spec/bundler/commands/show_spec.rb +++ b/spec/bundler/commands/show_spec.rb @@ -191,7 +191,30 @@ end context "with a valid regexp for gem name" do - it "presents alternatives", :readline do + it "returns the exact match without prompting when requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "show myrack --exact-match" + expect(out).to include(default_bundle_path("gems", "myrack-1.0.0").to_s) + end + + it "does not fall back to regexp matching when exact matching is requested" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + gem "myrack-obama" + G + + bundle "show rac --exact-match", raise_on_error: false + expect(err).to include("Could not find gem 'rac'.") + expect(out).not_to include("0 : - exit -") + end + + it "presents alternatives without the exact match flag", :readline do install_gemfile <<-G source "https://gem.repo1" gem "myrack" @@ -199,7 +222,9 @@ G bundle "show rac" - expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/) + expect(out).to include("1 : myrack") + expect(out).to include("2 : myrack-obama") + expect(out).to include("0 : - exit -") end end diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 05c9bf75b7753f..fa9df3eb9dfc10 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -6783,6 +6783,20 @@ rb_vm_opt_getconstant_path(rb_execution_context_t *ec, rb_control_frame_t *const return val; } +// Return true if the once value is already computed and set *result to the value. +// Otherwise, return false. +// Used for ZJIT. Keep in sync with `vm_once_dispatch` below. +bool +rb_vm_once_done_value(ISE is, VALUE *result) +{ + rb_thread_t *running_th = rbimpl_atomic_ptr_load((void**)&is->once.running_thread, RBIMPL_ATOMIC_ACQUIRE); + if (running_th == RUNNING_THREAD_ONCE_DONE) { + *result = is->once.value; + return true; + } + return false; +} + static VALUE vm_once_dispatch(rb_execution_context_t *ec, ISEQ iseq, ISE is) { diff --git a/yjit.c b/yjit.c index d59bfaa38108ee..5e08b31afc880b 100644 --- a/yjit.c +++ b/yjit.c @@ -49,6 +49,12 @@ STATIC_ASSERT(size_t_no_padding_bits, sizeof(size_t) == sizeof(uint64_t)); // support one scheme for simplicity. STATIC_ASSERT(pointer_tagging_scheme, USE_FLONUM); +enum yjit_bindgen_constants { + // ISEQ_TRANSLATED expands to an enum value through a chain of macros, + // which bindgen cannot evaluate, so it needs to be re-exposed here. + YJIT_ISEQ_TRANSLATED = ISEQ_TRANSLATED, +}; + // NOTE: We can trust that uint8_t has no "padding bits" since the C spec // guarantees it. Wording about padding bits is more explicit in C11 compared // to C99. See C11 7.20.1.1p2. All this is to say we have _some_ standards backing to diff --git a/yjit/bindgen/src/main.rs b/yjit/bindgen/src/main.rs index ff9e587484a1e3..e871924a7e4878 100644 --- a/yjit/bindgen/src/main.rs +++ b/yjit/bindgen/src/main.rs @@ -76,6 +76,7 @@ fn main() { .allowlist_type("RBasic") .allowlist_type("ruby_rstring_flags") + .allowlist_type("ruby_rstruct_flags") // This function prints info about a value and is useful for debugging .allowlist_function("rb_obj_info_dump") @@ -284,6 +285,7 @@ fn main() { .allowlist_function("rb_jit_vm_unlock") .allowlist_function("rb_jit_for_each_iseq") .allowlist_type("jit_bindgen_constants") + .allowlist_type("yjit_bindgen_constants") .allowlist_function("rb_vm_barrier") .allowlist_function("rb_yjit_cdhash_all_fixnum_p") .allowlist_function("rb_yjit_cdhash_lookup") @@ -407,12 +409,29 @@ fn main() { // Unwrap the Result and panic on failure. .expect("Unable to generate bindings"); + // Write to a Vec for post-processing + let mut bindings_string = Vec::new(); + bindings.write(Box::new(&mut bindings_string)).expect("Couldn't write bindings!"); + let mut bindings_string = String::from_utf8(bindings_string).expect("bindings should be UTF-8"); + + // Give some generated type aliases an integer type that is nicer to use from + // Rust than the one bindgen derives from C. + const TYPE_REPLACEMENTS: &[(&str, &str)] = &[ + // usize is what VALUE() takes, so flag masks need no cast at their use sites + ("pub type ruby_rstruct_flags = u32;", "pub type ruby_rstruct_flags = usize;"), + ]; + // Each needle is a whole line of the output, so plain replacement is unambiguous. + // Yes, this search-and-replace could be faster, but it's a small file. + for (needle, replacement) in TYPE_REPLACEMENTS { + assert!(bindings_string.contains(needle), "no line to replace: {needle}"); + bindings_string = bindings_string.replace(needle, replacement); + } + + // Write out to file let mut out_path: PathBuf = src_root; out_path.push("yjit"); out_path.push("src"); out_path.push("cruby_bindings.inc.rs"); - bindings - .write_to_file(out_path) - .expect("Couldn't write bindings!"); + std::fs::write(out_path, bindings_string).expect("file output failed"); } diff --git a/yjit/src/cruby.rs b/yjit/src/cruby.rs index dc8b3200aa9fd5..7f1826fa0cbf8a 100644 --- a/yjit/src/cruby.rs +++ b/yjit/src/cruby.rs @@ -750,12 +750,6 @@ mod manual_defs { pub const VM_CALL_ZSUPER : u32 = 1 << VM_CALL_ZSUPER_bit; pub const VM_CALL_OPT_SEND : u32 = 1 << VM_CALL_OPT_SEND_bit; - // From internal/struct.h - in anonymous enum, so we can't easily import it - pub const RSTRUCT_EMBED_LEN_MASK: usize = (RUBY_FL_USER7 | RUBY_FL_USER6 | RUBY_FL_USER5 | RUBY_FL_USER4 | RUBY_FL_USER3 |RUBY_FL_USER2 | RUBY_FL_USER1) as usize; - - // From iseq.h - via a different constant, which seems to confuse bindgen - pub const ISEQ_TRANSLATED: usize = RUBY_FL_USER8 as usize; - // We'll need to encode a lot of Ruby struct/field offsets as constants unless we want to // redeclare all the Ruby C structs and write our own offsetof macro. For now, we use constants. pub const RUBY_OFFSET_RBASIC_FLAGS: i32 = 0; // struct RBasic, field "flags" diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index a5302a9a56b17d..2b688521c80936 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -1060,8 +1060,13 @@ pub const DEFINED_REF: defined_type = 15; pub const DEFINED_FUNC: defined_type = 16; pub const DEFINED_CONST_FROM: defined_type = 17; pub type defined_type = u32; +pub const YJIT_ISEQ_TRANSLATED: yjit_bindgen_constants = 1048576; +pub type yjit_bindgen_constants = u32; pub type rb_seq_param_keyword_struct = rb_iseq_constant_body_rb_iseq_parameters_rb_iseq_param_keyword; +pub const RSTRUCT_EMBED_LEN_MASK: ruby_rstruct_flags = 1040384; +pub const RSTRUCT_EMBED_LEN_SHIFT: ruby_rstruct_flags = 13; +pub type ruby_rstruct_flags = usize; pub const ROBJECT_OFFSET_AS_HEAP_FIELDS: jit_bindgen_constants = 16; pub const ROBJECT_OFFSET_AS_ARY: jit_bindgen_constants = 16; pub const RCLASS_OFFSET_PRIME_FIELDS_OBJ: jit_bindgen_constants = 40; diff --git a/yjit/src/invariants.rs b/yjit/src/invariants.rs index 68eb84604259ff..784629b2e26a61 100644 --- a/yjit/src/invariants.rs +++ b/yjit/src/invariants.rs @@ -504,7 +504,7 @@ pub extern "C" fn rb_yjit_constant_ic_update(iseq: *const rb_iseq_t, ic: IC, ins // This should come from a running iseq, so direct threading translation // should have been done - assert!(unsafe { FL_TEST(iseq.into(), VALUE(ISEQ_TRANSLATED)) } != VALUE(0)); + assert!(unsafe { FL_TEST(iseq.into(), VALUE(YJIT_ISEQ_TRANSLATED as usize)) } != VALUE(0)); assert!(u32::from(insn_idx) < unsafe { get_iseq_encoded_size(iseq) }); // Ensure that the instruction the insn_idx is pointing to is in diff --git a/zjit.c b/zjit.c index 3e3c6e9c6dce5a..741c99c12a0b64 100644 --- a/zjit.c +++ b/zjit.c @@ -333,6 +333,7 @@ rb_zjit_class_has_default_allocator(VALUE klass) VALUE rb_vm_untag_block_handler(VALUE block_handler); VALUE rb_vm_get_untagged_block_handler(rb_control_frame_t *reg_cfp); +bool rb_vm_once_done_value(ISE is, VALUE *result); // Primitives used by zjit.rb. Don't put other functions below, which wouldn't use them. VALUE rb_zjit_enable(rb_execution_context_t *ec, VALUE self); diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 9463da3070bdc4..f1495d92a6f289 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -100,6 +100,7 @@ fn main() { .allowlist_var("RB_GC_ZJIT_FASTPATH_.*") .allowlist_type("ruby_rstring_flags") + .allowlist_type("ruby_rstruct_flags") // This function prints info about a value and is useful for debugging .allowlist_function("rb_raw_obj_info") @@ -276,6 +277,7 @@ fn main() { .allowlist_function("rb_callable_method_entry") .allowlist_function("rb_callable_method_entry_or_negative") .allowlist_function("rb_vm_frame_method_entry") + .allowlist_function("rb_vm_once_done_value") .allowlist_type("IVC") // pointer to iseq_inline_iv_cache_entry .allowlist_type("IC") // pointer to iseq_inline_constant_cache .allowlist_type("iseq_inline_constant_cache_entry") @@ -498,16 +500,21 @@ fn main() { // Write to a Vec for post-processing let mut bindings_string = Vec::new(); bindings.write(Box::new(&mut bindings_string)).expect("Couldn't write bindings!"); - - // Use i32 for this type since that's what the assembler APIs expect - const JIT_CONSTANTS_NEEDLE: &[u8] = b"pub type jit_bindgen_constants = u32;"; - const JIT_CONSTANTS_REPLACEMENT: &[u8] = b"pub type jit_bindgen_constants = i32;"; + let mut bindings_string = String::from_utf8(bindings_string).expect("bindings should be UTF-8"); + + // Give some generated type aliases an integer type that is nicer to use from + // Rust than the one bindgen derives from C. + const TYPE_REPLACEMENTS: &[(&str, &str)] = &[ + // i32 is what the assembler APIs expect + ("pub type jit_bindgen_constants = u32;", "pub type jit_bindgen_constants = i32;"), + // usize is what VALUE() takes, so flag masks need no cast at their use sites + ("pub type ruby_rstruct_flags = u32;", "pub type ruby_rstruct_flags = usize;"), + ]; + // Each needle is a whole line of the output, so plain replacement is unambiguous. // Yes, this search-and-replace could be faster, but it's a small file. - for line in bindings_string.as_mut_slice().split_mut(|&byte| byte == b'\n') { - if line == JIT_CONSTANTS_NEEDLE { - line.copy_from_slice(JIT_CONSTANTS_REPLACEMENT); - break; - } + for (needle, replacement) in TYPE_REPLACEMENTS { + assert!(bindings_string.contains(needle), "no line to replace: {needle}"); + bindings_string = bindings_string.replace(needle, replacement); } // Write out to file diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index f58f150a4a90bb..ad62deba76cac9 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -3976,12 +3976,25 @@ fn gen_function_stub(cb: &mut CodeBlock, iseq_call: IseqCallRef) -> Result reg, CArgLocation::StackSlot(slot) => { // The stub runs before any frame setup, so stack-passed arguments @@ -3993,7 +4006,7 @@ fn gen_function_stub(cb: &mut CodeBlock, iseq_call: IseqCallRef) -> Result bool; pub fn rb_vm_untag_block_handler(block_handler: VALUE) -> VALUE; 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; pub fn rb_iseq_encoded_size(iseq: *const rb_iseq_t) -> ::std::os::raw::c_uint; pub fn rb_iseq_pc_at_idx(iseq: *const rb_iseq_t, insn_idx: u32) -> *mut VALUE; pub fn rb_iseq_opcode_at_pc(iseq: *const rb_iseq_t, pc: *const VALUE) -> ::std::os::raw::c_int; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index e8df6678717c1c..23dddf35e6133f 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -673,6 +673,7 @@ pub enum SideExitReason { NoProfileGetIvar, NoProfileSetIvar, InvokeBlockNotIfunc, + OnceNotDone, } /// Marks a side exit as triggering profiling and recompilation. @@ -9404,6 +9405,20 @@ fn add_iseq_to_hir( } } } + YARVINSN_once => { + let iseq: *const rb_iseq_t = get_arg(pc, 0).as_ptr(); + let ise: *mut iseq_inline_storage_entry = get_arg(pc, 1).as_mut_ptr(); + debug_assert!(!iseq.is_null()); + debug_assert!(!ise.is_null()); + let mut value = Qnil; + if unsafe { rb_vm_once_done_value(ise, &mut value) } { + let val = fun.push_insn(block, Insn::Const { val: Const::Value(value) }); + state.stack_push(val); + } else { + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: Box::new(SideExitReason::OnceNotDone), recompile: Some(Recompile) }); + break; // End the block + } + } YARVINSN_branchunless | YARVINSN_branchunless_without_ints => { let offset = get_arg(pc, 0).as_i64(); if opcode == YARVINSN_branchunless && offset < 0 { diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index bbd0dfadb2e140..9758a5763c95a7 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -6273,6 +6273,67 @@ pub(crate) mod hir_build_tests { Return v22 "); } + + #[test] + fn test_once_not_done_side_exits() { + eval(" + def test = /#{'a'.upcase}/o + "); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + SideExit OnceNotDone recompile + "); + // Running the method fills in the once cache, so a recompile sees the + // cached value instead of side-exiting. + eval("test"); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:RegexpExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + CheckInterrupts + Return v10 + "); + } + + #[test] + fn test_once_done_returns_value() { + eval(" + def test = /#{'a'.upcase}/o + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:RegexpExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + CheckInterrupts + Return v10 + "); + } } /// Test successor and predecessor set computations. diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 6bf4b673f32638..23eb2acd4650f5 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -252,6 +252,7 @@ make_counters! { exit_directive_induced, exit_send_while_tracing, exit_invokeblock_not_ifunc, + exit_once_not_done, } // Send fallback counters that are summed as dynamic_send_count @@ -674,6 +675,7 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { NoProfileGetIvar => exit_no_profile_getivar, NoProfileSetIvar => exit_no_profile_setivar, InvokeBlockNotIfunc => exit_invokeblock_not_ifunc, + OnceNotDone => exit_once_not_done, } }