diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 537432bd407920..dbe2f43bb475c8 100644 --- a/.github/workflows/check_sast.yml +++ b/.github/workflows/check_sast.yml @@ -78,14 +78,14 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: languages: ${{ matrix.language }} build-mode: none config-file: .github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: category: '/language:${{ matrix.language }}' upload: False @@ -127,7 +127,7 @@ jobs: continue-on-error: true - name: Upload SARIF - uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: sarif_file: sarif-results/${{ matrix.language }}.sarif continue-on-error: true diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index d0a8024b053b0d..f0fa12c456ef08 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,4 +12,4 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 + - uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index e63a22e5d262b9..d2c8b7a4f1a929 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -73,6 +73,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: sarif_file: results.sarif diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 82cef518c3ea51..3ff9076d57bca0 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@50414676f9f5d50a65992c6dd2ed02641263226c # v2.82.10 + - uses: taiki-e/install-action@c7eb1735f09259a5035e8e5d44b1406b1cddc0fb # v2.83.0 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 81aa9dd4beaca7..223712af7cb87d 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@50414676f9f5d50a65992c6dd2ed02641263226c # v2.82.10 + - uses: taiki-e/install-action@c7eb1735f09259a5035e8e5d44b1406b1cddc0fb # v2.83.0 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/array.c b/array.c index a05d57b83105fa..58f41287bdb3cd 100644 --- a/array.c +++ b/array.c @@ -208,7 +208,9 @@ ary_embed_size(long capa) static bool ary_embeddable_p(long capa) { - return rb_gc_size_allocatable_p(ary_embed_size(capa)); + const long embed_len_max = RARRAY_EMBED_LEN_MASK >> RARRAY_EMBED_LEN_SHIFT; + + return capa <= embed_len_max && rb_gc_size_allocatable_p(ary_embed_size(capa)); } bool diff --git a/depend b/depend index 96d0485d527d51..8db2d7a6759f4f 100644 --- a/depend +++ b/depend @@ -16094,6 +16094,7 @@ scheduler.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h scheduler.$(OBJEXT): $(top_srcdir)/internal/serial.h scheduler.$(OBJEXT): $(top_srcdir)/internal/set_table.h scheduler.$(OBJEXT): $(top_srcdir)/internal/static_assert.h +scheduler.$(OBJEXT): $(top_srcdir)/internal/struct.h scheduler.$(OBJEXT): $(top_srcdir)/internal/thread.h scheduler.$(OBJEXT): $(top_srcdir)/internal/variable.h scheduler.$(OBJEXT): $(top_srcdir)/internal/vm.h @@ -16897,6 +16898,7 @@ signal.$(OBJEXT): $(top_srcdir)/internal/set_table.h signal.$(OBJEXT): $(top_srcdir)/internal/signal.h signal.$(OBJEXT): $(top_srcdir)/internal/static_assert.h signal.$(OBJEXT): $(top_srcdir)/internal/string.h +signal.$(OBJEXT): $(top_srcdir)/internal/struct.h signal.$(OBJEXT): $(top_srcdir)/internal/thread.h signal.$(OBJEXT): $(top_srcdir)/internal/variable.h signal.$(OBJEXT): $(top_srcdir)/internal/vm.h diff --git a/dir.rb b/dir.rb index 461b344d180ead..8917a8f79e4691 100644 --- a/dir.rb +++ b/dir.rb @@ -213,18 +213,29 @@ def initialize(name, encoding: nil) end # call-seq: - # Dir[patterns, base: nil, sort: true] -> array + # Dir[*patterns, base: nil, sort: true] -> array + # + # Like Dir.glob, but does not accept keyword argument +flags+, + # and may take multiple patterns as arguments: + # + # Dir['*.rb', '*.h'].take(3) # => ["KNOWNBUGS.rb", "array.rb", "ast.rb"] # - # Like Dir.glob, but does not accept keyword argument +flags+. def self.[](*args, base: nil, sort: true) Primitive.dir_s_aref(args, base, sort) end # call-seq: # Dir.glob(patterns, flags: 0, base: '.', sort: true) -> array_of_entries + # Dir.glob(patterns, flags: 0, base: '.', sort: true) {|entry_name| ... } -> nil # # Returns an array of filesystem entries; # see {Filename Globbing}[rdoc-ref:file/filename_globbing.md]. + # + # With a block given, calls the block with each of the selected entry names + # and returns +nil+: + # + # Dir.glob('*.rb') {|entry_name| puts entry_name } # => nil + # def self.glob(pattern, _flags = 0, flags: _flags, base: nil, sort: true) Primitive.attr! :use_block Primitive.dir_s_glob(pattern, flags, base, sort) diff --git a/doc/file/filename_globbing.md b/doc/file/filename_globbing.md index 8c691cccda50d1..7e21abeaa20dff 100644 --- a/doc/file/filename_globbing.md +++ b/doc/file/filename_globbing.md @@ -33,7 +33,10 @@ Inputs to the filename-globbing methods: Their return values: -- Each of the methods `Dir[]` and Dir.glob returns an array of the selected string entries. +- \Method `Dir[]` returns an array of the selected string entries. +- \Method Dir.glob with no block returns an array of the selected string entries. +- \Method Dir.glob with a block calls the block with each selected string entry + and returns `nil`. - \Method Pathname#glob with no block returns an array of Pathname objects each based on a selected string entry. - \Method Pathname#glob with a block calls the block with each pathname @@ -44,8 +47,13 @@ Examples: ```ruby Dir['*'].take(3) # => ["BSDL", "CONTRIBUTING.md", "COPYING"] +Dir['*.rb', '*.h'].take(3) +# => ["KNOWNBUGS.rb", "array.rb", "ast.rb"] Dir.glob('*').take(3) # => ["BSDL", "CONTRIBUTING.md", "COPYING"] +Dir.glob(['*.rb', '*.h']).take(3) +# => ["KNOWNBUGS.rb", "array.rb", "ast.rb"] +Dir.glob('*.rb') {|entry_name| puts entry_name } # => nil Pathname('.').glob('*').take(3) # => [#, #, #] a = [] @@ -299,6 +307,25 @@ Pathname('.').glob('test/ruby/**/*.rb').take(3) # #] ``` +The double-asterisk pattern matches directories recursively +only when followed by the slash character `'/'`; +otherwise it is equivalent to pattern `'*'`: + +```ruby +Dir.glob('**') == Dir.glob('*') # => true +``` + +A pattern ending with `'/'` matches directory names only; +each matched name ends with `'/'`: + +```ruby +# Find the directories at the top level. +Dir.glob('*/').take(3) # => ["basictest/", "benchmark/", "bin/"] + +# Find all directories everywhere. +Dir.glob('**/').take(3) # => ["basictest/", "benchmark/", "benchmark/gc/"] +``` + The pattern may be escaped: ```ruby @@ -325,6 +352,22 @@ Pathname('.').glob('\[k-m][h-j][a-c]') # => [] Pathname('.').glob('\**/*.rb') # => [] ``` +Note that the backslash escapes the following character on Windows as well, +and therefore cannot be used as a path separator in the pattern; +`Dir.glob('C:\Users\*')` matches nothing. +Write the pattern with forward slashes instead: + +```ruby +Dir.glob('C:/Users/*') +``` + +A Windows path taken from an external source (such as an environment variable) +may be converted for use as a pattern: + +```ruby +pattern = path.tr('\\', '/') +``` + ## Keyword Arguments | Keyword | Value | Default | Meaning | @@ -417,6 +460,11 @@ Pathname('.').glob('\*').size # => 0 Pathname('.').glob('\*', File::FNM_NOESCAPE).size # => 0 ``` +Note that on Windows this flag does not make the backslash usable +as a path separator in the pattern; +the backslash is then matched as an ordinary character, +which cannot occur in an entry name. + #### Constant File::FNM_SHORTNAME By default, Windows shortname matching is disabled; diff --git a/gc.c b/gc.c index e521369caa72ef..4bdc164347271b 100644 --- a/gc.c +++ b/gc.c @@ -1106,6 +1106,16 @@ VALUE class_allocate_complex_instance(VALUE klass, uint32_t capacity) return obj; } +static inline size_t +robject_embedded_size(uint32_t fields_count) +{ + size_t size = rb_obj_embedded_size(fields_count); + if (!rb_gc_size_allocatable_p(size)) { + size = sizeof(struct RObject); + } + return size; +} + VALUE rb_class_allocate_instance(VALUE klass) { @@ -1118,10 +1128,7 @@ rb_class_allocate_instance(VALUE klass) obj = class_allocate_complex_instance(klass, index_tbl_num_entries); } else { - size_t size = rb_obj_embedded_size(index_tbl_num_entries); - if (!rb_gc_size_allocatable_p(size)) { - size = sizeof(struct RObject); - } + size_t size = robject_embedded_size(index_tbl_num_entries); // There might be a NEWOBJ tracepoint callback, and it may set fields. // So the shape must be passed to `NEWOBJ_OF`. @@ -1145,6 +1152,25 @@ rb_class_allocate_instance(VALUE klass) return obj; } +#if USE_ZJIT +bool +rb_zjit_class_allocate_instance_fastpath(VALUE klass, size_t *size_out, shape_id_t *shape_id_out) +{ + uint32_t index_tbl_num_entries = RCLASS_MAX_IV_COUNT(klass); + + RUBY_ASSERT(rb_shape_max_capacity() > 0); + if (RB_UNLIKELY(index_tbl_num_entries > rb_shape_max_capacity())) { + return false; + } + + size_t size = robject_embedded_size(index_tbl_num_entries); + *size_out = size; + *shape_id_out = rb_shape_transition_slot_size(rb_shape_transition_robject(0), + rb_gc_size_slot_size(size)); + return true; +} +#endif + void rb_gc_register_pinning_obj(VALUE obj) { @@ -2159,16 +2185,10 @@ object_id(VALUE obj) return object_id0(obj); } -static inline void -obj_free_object_id(VALUE obj) -{ -} - void rb_gc_obj_free_vm_weak_references(VALUE obj) { ASSUME(!RB_SPECIAL_CONST_P(obj)); - obj_free_object_id(obj); if (rb_obj_gen_fields_p(obj)) { rb_free_generic_ivar(obj); @@ -2636,11 +2656,22 @@ ruby_stack_check(void) /* ==================== Marking ==================== */ +/* The traversal mark redirect is per-Ractor, except on a modular GC where + * marking can run on worker threads with no current EC and it lives in the VM. + * GC_MARK_FUNC_DATA_SLOTP() points at the active slot; a non-modular build pays + * nothing extra over a plain GET_VM() (one GET_RACTOR(), no NULL check). */ +#if USE_MODULAR_GC +# define GC_MARK_FUNC_DATA_SLOTP() (&GET_VM()->gc.mark_func_data) +#else +# define GC_MARK_FUNC_DATA_SLOTP() (&GET_RACTOR()->mark_func_data) +#endif + #define RB_GC_MARK_OR_TRAVERSE(func, obj_or_ptr, obj, check_obj) do { \ if (!RB_SPECIAL_CONST_P(obj)) { \ - rb_vm_t *vm = GET_VM(); \ - void *objspace = vm->gc.objspace; \ - if (LIKELY(vm->gc.mark_func_data == NULL)) { \ + struct gc_mark_func_data_struct **mfdp = GC_MARK_FUNC_DATA_SLOTP(); \ + struct gc_mark_func_data_struct *mark_func_data = *mfdp; \ + void *objspace = GET_VM()->gc.objspace; \ + if (LIKELY(mark_func_data == NULL)) { \ GC_ASSERT(rb_gc_impl_during_gc_p(objspace)); \ (func)(objspace, (obj_or_ptr)); \ } \ @@ -2649,10 +2680,9 @@ ruby_stack_check(void) !rb_gc_impl_garbage_object_p(objspace, obj) : \ true) { \ GC_ASSERT(!rb_gc_impl_during_gc_p(objspace)); \ - struct gc_mark_func_data_struct *mark_func_data = vm->gc.mark_func_data; \ - vm->gc.mark_func_data = NULL; \ + *mfdp = NULL; \ mark_func_data->mark_func((obj), mark_func_data->data); \ - vm->gc.mark_func_data = mark_func_data; \ + *mfdp = mark_func_data; \ } \ } \ } while (0) @@ -4519,16 +4549,16 @@ rb_objspace_reachable_objects_from(VALUE obj, void (func)(VALUE, void *), void * if (rb_gc_impl_during_gc_p(rb_gc_get_objspace())) rb_bug("rb_objspace_reachable_objects_from() is not supported while during GC"); if (!RB_SPECIAL_CONST_P(obj)) { - rb_vm_t *vm = GET_VM(); - struct gc_mark_func_data_struct *prev_mfd = vm->gc.mark_func_data; + struct gc_mark_func_data_struct **mfdp = GC_MARK_FUNC_DATA_SLOTP(); + struct gc_mark_func_data_struct *prev_mfd = *mfdp; struct gc_mark_func_data_struct mfd = { .mark_func = func, .data = data, }; - vm->gc.mark_func_data = &mfd; + *mfdp = &mfd; rb_gc_mark_children(rb_gc_get_objspace(), obj); - vm->gc.mark_func_data = prev_mfd; + *mfdp = prev_mfd; } } } @@ -4558,16 +4588,17 @@ rb_objspace_reachable_objects_from_root(void (func)(const char *category, VALUE, .data = passing_data, }; - struct gc_mark_func_data_struct *prev_mfd = vm->gc.mark_func_data; + struct gc_mark_func_data_struct **mfdp = GC_MARK_FUNC_DATA_SLOTP(); + struct gc_mark_func_data_struct *prev_mfd = *mfdp; struct gc_mark_func_data_struct mfd = { .mark_func = root_objects_from, .data = &data, }; - vm->gc.mark_func_data = &mfd; + *mfdp = &mfd; rb_gc_save_machine_context(); rb_gc_mark_roots(vm->gc.objspace, &data.category); - vm->gc.mark_func_data = prev_mfd; + *mfdp = prev_mfd; } /* diff --git a/io.c b/io.c index fb66a2f27fd51b..91119735508474 100644 --- a/io.c +++ b/io.c @@ -12141,7 +12141,7 @@ io_s_foreach(VALUE v) * * - {Open Options}[rdoc-ref:IO@Open+Options]. * - {Encoding options}[rdoc-ref:encodings.rdoc@Encoding+Options]. - * - {Line Options}[rdoc-ref:IO@Line+IO]. + * - {Line Input Options}[rdoc-ref:IO@Line+Input+Options]. * * Returns an Enumerator if no block is given. * @@ -12217,7 +12217,7 @@ io_s_readlines(VALUE v) * * - {Open Options}[rdoc-ref:IO@Open+Options]. * - {Encoding options}[rdoc-ref:encodings.rdoc@Encoding+Options]. - * - {Line Options}[rdoc-ref:IO@Line+IO]. + * - {Line Input Options}[rdoc-ref:IO@Line+Input+Options]. * */ @@ -15253,7 +15253,12 @@ set_LAST_READ_LINE(VALUE val, ID _x, VALUE *_y) * === Line Input * * Class \IO supports line-oriented input for - * {files}[rdoc-ref:IO@File+Line+Input] and {IO streams}[rdoc-ref:IO@Stream+Line+Input] + * {files}[rdoc-ref:IO@File+Line+Input] and {IO streams}[rdoc-ref:IO@Stream+Line+Input]. + * + * ==== Line Input Options + * + * Optional keyword argument +chomp+ (default: +false+) + * specifies whether line separators are to be excluded from the result of a read. * * ==== \File Line Input * diff --git a/jit.c b/jit.c index 5def0f89b14ec7..534a07c848abfd 100644 --- a/jit.c +++ b/jit.c @@ -41,6 +41,9 @@ enum jit_bindgen_constants { // Field offset for the RHash struct RUBY_OFFSET_RHASH_IFNONE = offsetof(struct RHash, ifnone), + // Max pairs an embedded ar_table hash holds before it converts to an st_table + RUBY_RHASH_AR_TABLE_MAX_SIZE = RHASH_AR_TABLE_MAX_SIZE, + // Field offsets for the RString struct RUBY_OFFSET_RSTRING_LEN = offsetof(struct RString, len), diff --git a/lib/bundler/installer/parallel_installer.rb b/lib/bundler/installer/parallel_installer.rb index f65f171cb8a8f2..09540c3f568306 100644 --- a/lib/bundler/installer/parallel_installer.rb +++ b/lib/bundler/installer/parallel_installer.rb @@ -125,7 +125,7 @@ def with_jobserver # every native extension build with `fatal error U1065: invalid option # '-'`. Skip the jobserver when nmake is in use. Other Windows toolchains # such as mingw use GNU make and keep working through the inherited pipe. - return yield if nmake? + return yield if nmake? || bsd_make? begin r, w = IO.pipe @@ -155,6 +155,12 @@ def nmake? /\bnmake/i.match?(make.to_s) end + def bsd_make? + return false unless Gem.freebsd_platform? + make = ENV["MAKE"] || ENV["make"] || "make" + !/\bgmake/i.match?(make) + end + def install_serially until finished_installing? raise "failed to find a spec to enqueue while installing serially" unless spec_install = @specs.find(&:ready_to_enqueue?) diff --git a/lib/net/http.rb b/lib/net/http.rb index 0e8d8bf3fa869f..4e2e6c9263f1d9 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -82,6 +82,7 @@ class HTTPHeaderSyntaxError < StandardError; end # body = 'Some text' # http.post(path, body) # Can also have a block. # http.put(path, body) + # http.query(path, body) # Can also have a block. # http.delete(path) # http.options(path) # http.trace(path) @@ -207,6 +208,7 @@ class HTTPHeaderSyntaxError < StandardError; end # - #options: OPTIONS. # - #trace: TRACE. # - #patch: PATCH. + # - #query, #request_query: QUERY. # # - {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]: # @@ -553,6 +555,8 @@ class HTTPHeaderSyntaxError < StandardError; end # Sends a PROPPATCH request and returns a response object. # - {#put}[rdoc-ref:Net::HTTP#put]: # Sends a PUT request and returns a response object. + # - {#query}[rdoc-ref:Net::HTTP#query]: + # Sends a QUERY request and returns a response object. # - {#request}[rdoc-ref:Net::HTTP#request]: # Sends a request and returns a response object. # - {#request_get}[rdoc-ref:Net::HTTP#request_get]: @@ -2101,6 +2105,25 @@ def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segm send_entity(path, data, initheader, dest, Patch, &block) end + # Sends a QUERY request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Query object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Net::HTTP.new(hostname) + # http.query('/todos/1', data) # => # + # + # Related: + # + # - Net::HTTP::Query: request class for \HTTP method QUERY. + # - Net::HTTP.query: sends QUERY request, returns response body. + # + def query(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + send_entity(path, data, initheader, dest, Query, &block) + end + # Sends a PUT request to the server; # returns an instance of a subclass of Net::HTTPResponse. # @@ -2335,6 +2358,36 @@ def request_put(path, data, initheader = nil, &block) #:nodoc: request Put.new(path, initheader), data, &block end + # Sends a QUERY request to the server; + # returns an instance of a subclass of Net::HTTPResponse. + # + # The request is based on the Net::HTTP::Query object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # http = Net::HTTP.new(hostname) + # http.query('/todos/1', 'xyzzy') + # # => # + # + # With no block given, returns the response object: + # + # http = Net::HTTP.new(hostname) + # http.request_query('/todos') # => # + # + # With a block given, calls the block with the response object + # and returns the response object: + # + # http.request_query('/todos') do |res| + # p res + # end # => # + # + # Output: + # + # # + # + def request_query(path, data, initheader = nil, &block) # :yield: +response+ + request Query.new(path, initheader), data, &block + end + alias get2 request_get #:nodoc: obsolete alias head2 request_head #:nodoc: obsolete alias post2 request_post #:nodoc: obsolete @@ -2430,7 +2483,7 @@ def send_entity(path, data, initheader, dest, type, &block) # :stopdoc: - IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/.freeze # :nodoc: + IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE QUERY/.freeze # :nodoc: def transport_request(req) count = 0 diff --git a/lib/net/http/request.rb b/lib/net/http/request.rb index 4a138572e905f4..db7641a7b24dff 100644 --- a/lib/net/http/request.rb +++ b/lib/net/http/request.rb @@ -61,6 +61,7 @@ # - Net::HTTP::Options # - Net::HTTP::Trace # - Net::HTTP::Patch +# - Net::HTTP::Query # # Subclasses for WebDAV requests: # diff --git a/lib/net/http/requests.rb b/lib/net/http/requests.rb index 8dc79a9f665d52..2685b797ee09bd 100644 --- a/lib/net/http/requests.rb +++ b/lib/net/http/requests.rb @@ -271,6 +271,41 @@ class Net::HTTP::Patch < Net::HTTPRequest RESPONSE_HAS_BODY = true end +# \Class for representing +# {HTTP method QUERY}[https://www.rfc-editor.org/rfc/rfc10008.html]: +# +# require 'net/http' +# uri = URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# uri.path = '/posts' +# req = Net::HTTP::Query.new(uri) # => # +# req.body = '{"title": "foo","body": "bar","userId": 1}' +# req.content_type = 'application/json' +# res = Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: yes. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/HTTP#Safe_method]: yes. +# - {Idempotent}[https://en.wikipedia.org/wiki/HTTP#Idempotent_method]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/HTTP#Cacheable_method]: yes. +# +# Related: +# +# - Net::HTTP#query: sends +QUERY+ request, returns response object. +# +class Net::HTTP::Query < Net::HTTPRequest + # :stopdoc: + METHOD = 'QUERY' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + # # WebDAV methods --- RFC2518 # diff --git a/man/erb.1 b/man/erb.1 index 9d8cfbb1bb361d..f518d86678e44b 100644 --- a/man/erb.1 +++ b/man/erb.1 @@ -1,5 +1,5 @@ .\"Ruby is copyrighted by Yukihiro Matsumoto . -.Dd June 07, 2026 +.Dd July 10, 2026 .Dt ERB 1 "Ruby Programmer's Reference Guide" .Os UNIX .Sh NAME @@ -31,7 +31,7 @@ is a part of .Nm Ruby . .Pp .Sh OPTIONS -.Bl -tag -width "1234567890123" -compact +.Bl -tag -width "--encoding" -compact .Pp .It Fl -version Prints the version of diff --git a/man/goruby.1 b/man/goruby.1 index fb72f6d5b845a3..7465c20f1f5904 100644 --- a/man/goruby.1 +++ b/man/goruby.1 @@ -1,5 +1,5 @@ .\"Ruby is copyrighted by Yukihiro Matsumoto . -.Dd June 07, 2026 +.Dd May 24, 2026 .Dt GORUBY 1 "Ruby Programmer's Reference Guide" .Os UNIX .Sh NAME diff --git a/man/ruby.1 b/man/ruby.1 index 803e922c394296..84ec097dbf8ccd 100644 --- a/man/ruby.1 +++ b/man/ruby.1 @@ -1,5 +1,5 @@ .\"Ruby is copyrighted by Yukihiro Matsumoto . -.Dd June 07, 2026 +.Dd July 10, 2026 .Dt RUBY 1 "Ruby Programmer's Reference Guide" .Os UNIX .Sh NAME @@ -141,7 +141,7 @@ to see how they are being developed and used. The Ruby interpreter accepts the following command-line options (switches). They are quite similar to those of .Xr perl 1 . -.Bl -tag -width "1234567890123" -compact +.Bl -tag -width "--encoding" -compact .Pp .It Fl -copyright Prints the copyright notice, and quits immediately without running any @@ -808,6 +808,7 @@ When set to .Li 0 or left unset, the fast fallback feature is enabled. Introduced in Ruby 3.4, default: unset. +.El .Sh SEE ALSO .Bl -hang -compact -width "https://www.ruby-toolbox.com/" .It Lk https://www.ruby-lang.org/ diff --git a/object.c b/object.c index 5a3203d4e1a567..919a2fd2e7cdec 100644 --- a/object.c +++ b/object.c @@ -3571,7 +3571,6 @@ rb_cstr_to_dbl_raise(const char *p, rb_encoding *enc, int badcheck, int raise, i d = strtod(p, &end); if (errno == ERANGE) { OutOfRange(); - rb_warning("Float %.*s%s out of range", w, p, ellipsis); errno = 0; } if (p == end) { @@ -3654,7 +3653,6 @@ rb_cstr_to_dbl_raise(const char *p, rb_encoding *enc, int badcheck, int raise, i d = strtod(p, &end); if (errno == ERANGE) { OutOfRange(); - rb_warning("Float %.*s%s out of range", w, p, ellipsis); errno = 0; } if (badcheck) { diff --git a/pathname_builtin.rb b/pathname_builtin.rb index ab3132725db541..17c924cf8e41db 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -982,10 +982,10 @@ class Pathname # * File * # :markup: markdown # # call-seq: - # each_line(sep = $/, **opts) {|line| ... } → nil - # each_line(limit, **opts) {|line| ... } → nil - # each_line(sep, limit, **opts) {|line| ... } → nil - # each_line(...) → new_enumerator + # each_line(sep = $/, **opts) {|line| ... } -> nil + # each_line(limit, **opts) {|line| ... } -> nil + # each_line(sep, limit, **opts) {|line| ... } -> nil + # each_line(...) -> new_enumerator # # With a block given, calls the block with each line # from the file represented by `self`; @@ -1077,7 +1077,60 @@ def read(...) File.read(@path, ...) end # def binread(...) File.binread(@path, ...) end - # See File.readlines. Returns all the lines from the file. + # :markup: markdown + # + # call-seq: + # readlines(sep = $/, **options) -> array + # readlines(limit, **options) -> array + # readlines(sep, limit, **options) -> array + # + # Returns an array of all lines read from the source at the path in `self`, + # which must be the path to a file. + # + # Examples here use a file defined + # at [IO Example Files](rdoc-ref:IO@Example+Files). + # + # With no arguments given, parses lines from the file at the given path, + # as determined by the default line separator, and returns those lines in an array: + # + # ```ruby + # pn = Pathname('t.txt') + # ppn.readlines + # # => ["First line\n", "Second line\n", "\n", "Fourth line\n", "Fifth line\n"] + # ``` + # + # With argument `sep` given, + # parses lines as determined by that line separator + # (see [IO Line Separator](rdoc-ref:IO@Line+Separator)): + # + # ```ruby + # pn.readlines('li') + # # => ["First li", "ne\nSecond li", "ne\n\nFourth li", "ne\nFifth li", "ne\n"] + # pn.readlines('') # Special "paragraphs" separator value. + # # => ["First line\nSecond line\n\n", "Fourth line\nFifth line\n"] + # pn.readlines(nil) # Special "slurp" separator value. + # # => ["First line\nSecond line\n\nFourth line\nFifth line\n"] + # ``` + # + # With argument `limit` given, parses lines as determined by the default line separator + # and the given line-length `limit` + # (see [IO Line Separator](rdoc-ref:IO@Line+Separator) + # and [IO Line Limit](rdoc-ref:IO@Line+Limit)): + # + # ```ruby + # pn.readlines(7) + # # => ["First l", "ine\n", "Second ", "line\n", "\n", "Fourth ", "line\n", "Fifth l", "ine\n"] + # ``` + # + # With arguments `sep` and `limit` given, combines the two behaviors + # (see [IO Line Separator and Line Limit](rdoc-ref:IO@Line+Separator+and+Line+Limit)). + # + # Optional keyword arguments `options` specify: + # + # - {IO Open Options}[rdoc-ref:IO@Open+Options]. + # - {Encoding options}[rdoc-ref:encodings.rdoc@Encoding+Options]. + # - {IO Line Input Options}[rdoc-ref:IO@Line+Input+Options]. + # def readlines(...) File.readlines(@path, ...) end # See File.sysopen. @@ -1510,7 +1563,47 @@ def open(...) # :yield: file # def readlink() self.class.new(File.readlink(@path)) end - # See File.rename. Rename the file. + # :markup: markdown + # + # call-seq: + # rename(new_name) + # + # Renames the entry at the path in `self` to the entry given in `new_name`, + # which may be either a path or another pathname: + # + # ```ruby + # # Create source and destination pathnames and directories. + # pn_srcdir = Pathname('/tmp/src') # => # + # pn_srcdir.mkdir + # pn_dstdir = Pathname('/tmp/dst') # => # + # pn_dstdir.mkdir + # # Create source file pathname and file. + # pn_srcfile = pn_srcdir.join('t.tmp') # => # + # pn_srcfile.write('foo') + # # Create destination file pathname. + # pn_dstfile = pn_dstdir.join('u.tmp') # => # + # # Rename source file as destination file. + # pn_srcfile.rename(pn_dstfile) + # pn_srcfile.exist? # => false + # pn_dstfile.exist? # => true + # ``` + # + # Works for directories, too: + # + # ```ruby + # pn_dstdir.rename('/tmp/foo') + # pn_dstdir.exist? # => false + # Pathname('/tmp/foo').exist? # => true + # ``` + # + # Clean up. + # + # ```ruby + # pn_srcdir.rmtree + # Pathname('/tmp/foo').rmtree + # ``` + # + # Raises SystemCallError if the entry cannot be renamed. def rename(to) File.rename(@path, to) end # See File.stat. Returns a File::Stat object. @@ -2087,8 +2180,8 @@ def zero?() FileTest.zero?(@path) end class Pathname # call-seq: - # glob(patterns, base: '.', flags: 0, sort: true) → array_of_pathnames - # glob(patterns, base: '.', flags: 0, sort: true) {|pathname| ... } → nil + # glob(patterns, base: '.', flags: 0, sort: true) -> array_of_pathnames + # glob(patterns, base: '.', flags: 0, sort: true) {|pathname| ... } -> nil # # Selects filesystem entries # based on the given keyword arguments +base+, +flags+, and +sort+; diff --git a/ractor.c b/ractor.c index 22b6432c4b1e81..e58a85d100ea7d 100644 --- a/ractor.c +++ b/ractor.c @@ -237,6 +237,12 @@ ractor_mark(void *ptr) if (!checking_shareable) { // may unshareable objects + + /* objects this Ractor pinned via rb_gc_register_mark_object (the + * pin_array_list wrapper itself is an unshareable internal object; + * updated in ractor_update_references) */ + if (r->mark_object_ary) rb_gc_mark_movable(r->mark_object_ary); + rb_gc_mark(r->r_stdin); rb_gc_mark(r->r_stdout); rb_gc_mark(r->r_stderr); @@ -313,13 +319,23 @@ ractor_memsize(const void *ptr) return sizeof(rb_ractor_t) + ractor_sync_memsize(r); } +static void +ractor_update_references(void *ptr) +{ + rb_ractor_t *r = (rb_ractor_t *)ptr; + /* the registered mark objects list is marked movable in ractor_mark */ + if (r->mark_object_ary) { + r->mark_object_ary = rb_gc_location(r->mark_object_ary); + } +} + static const rb_data_type_t ractor_data_type = { "ractor", { ractor_mark, ractor_free, ractor_memsize, - NULL, // update + ractor_update_references, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY /* | RUBY_TYPED_WB_PROTECTED */ }; @@ -437,6 +453,9 @@ vm_remove_ractor(rb_vm_t *vm, rb_ractor_t *cr) RB_VM_LOCK(); { + /* keep this Ractor's registered mark objects alive under the main Ractor */ + if (cr->mark_object_ary) rb_vm_ractor_migrate_mark_objects(vm->ractor.main_ractor, cr); + RUBY_DEBUG_LOG("ractor.cnt:%u-- terminate_waiting:%d", vm->ractor.cnt, vm->ractor.sync.terminate_waiting); diff --git a/ractor_core.h b/ractor_core.h index 032e578603a398..ce43ca9e91943c 100644 --- a/ractor_core.h +++ b/ractor_core.h @@ -69,6 +69,17 @@ struct rb_ractor_struct { struct rb_ractor_pub pub; struct rb_ractor_sync sync; + /* objects pinned via rb_gc_register_mark_object; this Ractor owns them and + * marks them, and hands them to the main Ractor when it terminates. */ + VALUE mark_object_ary; + +#if !USE_MODULAR_GC + /* traversal-API mark redirect (NULL outside a traversal). Per Ractor so a + * concurrent traversal on another Ractor is never observed. A modular GC + * keeps this in the VM instead (vm->gc.mark_func_data). */ + struct gc_mark_func_data_struct *mark_func_data; +#endif + // thread management struct { struct ccan_list_head set; @@ -136,6 +147,7 @@ rb_ractor_self(const rb_ractor_t *r) rb_ractor_t *rb_ractor_main_alloc(void); void rb_ractor_main_setup(rb_vm_t *vm, rb_ractor_t *main_ractor, rb_thread_t *main_thread); +void rb_vm_ractor_migrate_mark_objects(rb_ractor_t *dst, rb_ractor_t *src); void rb_ractor_atexit(rb_execution_context_t *ec, VALUE result); void rb_ractor_atexit_exception(rb_execution_context_t *ec); void rb_ractor_teardown(rb_execution_context_t *ec); diff --git a/set.c b/set.c index e5853e52b44a93..4e76750a9f0400 100644 --- a/set.c +++ b/set.c @@ -416,7 +416,7 @@ set_s_alloc(VALUE klass) * call-seq: * Set[*objects] -> new_set * - * Returns a new \Set object populated with the given +objects+: + * Returns a new set populated with the given +objects+: * * Set[1, 'one', :one, 1.0, %w[a b c], {foo: 0, bar: 1}] * # => Set[1, "one", :one, 1.0, ["a", "b", "c"], {foo: 0, bar: 1}] @@ -503,11 +503,11 @@ set_initialize_with_block(RB_BLOCK_CALL_FUNC_ARGLIST(i, set)) * Set.new(object = nil) -> new_set * Set.new(object = nil) {|element| ... } -> new_set * - * Returns a new \Set object based on the given +object+, + * Returns a new set based on the given +object+, * which must be an Enumerable or +nil+. * * With argument +object+ given as +nil+, - * returns a new empty \Set object: + * returns a new empty set: * * Set.new # => Set[] * Set.new { fail 'Cannot happen' } # => Set[] # Block not called. @@ -695,7 +695,7 @@ set_i_to_a(VALUE set) * set.to_set.equal?(set) # => true * * With no block given, when +self+ is an instance of a subclass of +Set+, - * returns a \Set object containing the elements of +self+: + * returns a set containing the elements of +self+: * * class MySet < Set; end * my_set = MySet[*0..9] # => # @@ -749,7 +749,7 @@ set_i_join(int argc, VALUE *argv, VALUE set) * call-seq: * add(object) -> self * - * Adds the given +object+ to +self+, returns +self+: + * Adds the given +object+ to +self+; returns +self+: * * set = Set[0, 1, 2] * set.add(%w[a b c]) # => Set[0, 1, 2, ["a", "b", "c"]] @@ -1439,7 +1439,7 @@ set_xor_i(st_data_t key, st_data_t data) * call-seq: * self ^ enumerable -> new_set * - * Returns a new \Set object containing + * Returns a new set containing * the {exclusive OR}[https://en.wikipedia.org/wiki/Exclusive_or] * of +self+ and the given +enumerable+; * that is, containing each element that is in either +self+ or +enumerable+, @@ -1479,7 +1479,7 @@ set_i_xor(VALUE set, VALUE other) * call-seq: * self | enumerable -> new_set * - * Returns a new \Set object containing + * Returns a new set containing * the {union}[https://en.wikipedia.org/wiki/Union_(set_theory)] * of +self+ and the given +enumerable+; * that is, containing the elements of both +self+ and +enumerable+. @@ -1738,7 +1738,7 @@ set_i_replace(VALUE set, VALUE other) * call-seq: * reset -> self * - * Resets the internal state of +self+; return +self+. + * Resets the internal state of +self+; returns +self+. * * A set relies on the #hash results of each element being consistent. * Modifying an element in a way that changes the results of #hash @@ -2501,8 +2501,8 @@ rb_set_size(VALUE set) * - #each: * Calls the block with each successive element of +self+; returns +self+. * - #reset: - * Resets the internal state; useful if an element - * has been modified while an element in the set. + * Resets the internal state of +self+; returns +self+. + * Useful if an element has been modified while an element in the set. * */ void diff --git a/signal.c b/signal.c index f37aaf971ad997..6ec02a5fe8ad3a 100644 --- a/signal.c +++ b/signal.c @@ -1112,7 +1112,7 @@ rb_signal_exec(rb_thread_t *th, int sig) if (cmd == 0) { switch (sig) { case SIGINT: - rb_interrupt(); + rb_threadptr_interrupt_raise(th); break; #ifdef SIGHUP case SIGHUP: diff --git a/spec/bundler/bundler/installer/parallel_installer_spec.rb b/spec/bundler/bundler/installer/parallel_installer_spec.rb index 1f057cc4061145..22c880952aefbc 100644 --- a/spec/bundler/bundler/installer/parallel_installer_spec.rb +++ b/spec/bundler/bundler/installer/parallel_installer_spec.rb @@ -251,4 +251,50 @@ def redefine_build_jobs expect(makeflags_during).to eq(makeflags_before) end end + + describe "make jobserver on BSD" do + # BSD make (the default `make` on FreeBSD) can't parse the GNU + # `--jobserver-auth` and aborts every native extension build, so the + # jobserver must be skipped there. + it "leaves MAKEFLAGS untouched" do + parallel_installer = Bundler::ParallelInstaller.new(nil, [], 5, false, false) + + makeflags_before = ENV["MAKEFLAGS"] + makeflags_during = :not_yielded + + old_make = ENV["MAKE"] + ENV.delete("MAKE") + allow(Gem).to receive(:freebsd_platform?).and_return(true) + begin + parallel_installer.send(:with_jobserver) do + makeflags_during = ENV["MAKEFLAGS"] + end + ensure + ENV["MAKE"] = old_make + end + + expect(makeflags_during).to eq(makeflags_before) + end + + # A BSD user who opts into gmake gets a make that understands the + # jobserver, so it should still be set up. + it "sets up the jobserver when gmake is used" do + parallel_installer = Bundler::ParallelInstaller.new(nil, [], 5, false, false) + + makeflags_during = :not_yielded + + old_make = ENV["MAKE"] + ENV["MAKE"] = "gmake" + allow(Gem).to receive(:freebsd_platform?).and_return(true) + begin + parallel_installer.send(:with_jobserver) do + makeflags_during = ENV["MAKEFLAGS"] + end + ensure + ENV["MAKE"] = old_make + end + + expect(makeflags_during).to include("--jobserver-auth=") + end + end end diff --git a/spec/ruby/core/string/modulo_spec.rb b/spec/ruby/core/string/modulo_spec.rb index 45381fd92f5cb0..9c5eed8cf690fd 100644 --- a/spec/ruby/core/string/modulo_spec.rb +++ b/spec/ruby/core/string/modulo_spec.rb @@ -67,18 +67,20 @@ end end - it "raises an ArgumentError for unused arguments when $DEBUG is true" do - begin - old_debug = $DEBUG - $DEBUG = true - s = $stderr - $stderr = IOStub.new - - -> { "" % [1, 2, 3] }.should.raise(ArgumentError) - -> { "%s" % [1, 2, 3] }.should.raise(ArgumentError) - ensure - $DEBUG = old_debug - $stderr = s + ruby_version_is ""..."4.1" do + it "raises an ArgumentError for unused arguments when $DEBUG is true" do + begin + old_debug = $DEBUG + $DEBUG = true + s = $stderr + $stderr = IOStub.new + + -> { "" % [1, 2, 3] }.should.raise(ArgumentError) + -> { "%s" % [1, 2, 3] }.should.raise(ArgumentError) + ensure + $DEBUG = old_debug + $stderr = s + end end end diff --git a/spec/ruby/core/thread/handle_interrupt_spec.rb b/spec/ruby/core/thread/handle_interrupt_spec.rb index aa03d4c66d2570..c9de57aaa1ce60 100644 --- a/spec/ruby/core/thread/handle_interrupt_spec.rb +++ b/spec/ruby/core/thread/handle_interrupt_spec.rb @@ -122,4 +122,50 @@ def make_handle_interrupt_thread(interrupt_config, blocking = true) it "supports multiple pairs in the Hash" do make_handle_interrupt_thread(ArgumentError => :never, RuntimeError => :never).should == [:deferred] end + + ruby_version_is "4.1" do + platform_is_not :windows do + def signal_exception_deferred_by_handle_interrupt(signal) + ruby_exe(<<-RUBY) + waiting = Thread::Queue.new + release = Thread::Queue.new + inner = false + + Thread.new do + waiting.pop + Process.kill(:#{signal}, Process.pid) + release.push(true) + end + + begin + Thread.handle_interrupt(SignalException => :never) do + begin + waiting.push(true) + release.pop + rescue SignalException + inner = true + raise + end + end + rescue SignalException => exception + puts exception.class + puts exception.signo + puts inner + end + RUBY + end + + it "defers the default SIGINT Interrupt until exiting the handle_interrupt block" do + out = signal_exception_deferred_by_handle_interrupt(:INT) + + out.should == "Interrupt\n#{Signal.list.fetch("INT")}\nfalse\n" + end + + it "defers the default SIGTERM SignalException until exiting the handle_interrupt block" do + out = signal_exception_deferred_by_handle_interrupt(:TERM) + + out.should == "SignalException\n#{Signal.list.fetch("TERM")}\nfalse\n" + end + end + end end diff --git a/sprintf.c b/sprintf.c index d380053fa8fc37..9ff8498143127c 100644 --- a/sprintf.c +++ b/sprintf.c @@ -947,7 +947,6 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) */ if (posarg >= 0 && nextarg < argc && !(argc == 2 && RB_TYPE_P(argv[1], T_HASH))) { const char *mesg = "too many arguments for format string"; - if (RTEST(ruby_debug)) rb_raise(rb_eArgError, "%s", mesg); if (RTEST(ruby_verbose)) rb_warn("%s", mesg); } rb_str_resize(result, blen); diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb index 477c3415b48f53..e5028d4ef6a0ca 100644 --- a/test/net/http/test_http.rb +++ b/test/net/http/test_http.rb @@ -636,6 +636,7 @@ module TestNetHTTP_version_1_2_methods def test_request start {|http| _test_request__GET http + _test_request__QUERY http _test_request__accept_encoding http _test_request__file http # _test_request__range http # WEBrick does not support Range: header. @@ -665,6 +666,21 @@ def _test_request__GET(http) } end + def _test_request__QUERY(http) + data = 'query data' + req = Net::HTTP::Query.new('/') + req['Accept'] = $test_net_http_data_type + req['Content-Type'] = 'application/x-www-form-urlencoded' + http.request(req, data) {|res| + assert_kind_of Net::HTTPResponse, res + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_equal data.size, res['content-length'].to_i + end + assert_kind_of String, res.body + assert_equal data, res.body + } + end + def _test_request__accept_encoding(http) req = Net::HTTP::Get.new('/', 'accept-encoding' => 'deflate') http.request(req) {|res| @@ -791,6 +807,7 @@ def _test_request__uri_host(http) def test_send_request start {|http| _test_send_request__GET http + _test_send_request__QUERY http _test_send_request__HEAD http _test_send_request__POST http } @@ -806,6 +823,17 @@ def _test_send_request__GET(http) assert_equal $test_net_http_data, res.body end + def _test_send_request__QUERY(http) + data = 'aaabbb cc ddddddddddd lkjoiu4j3qlkuoa' + res = http.send_request('QUERY', '/', data, 'content-type' => 'application/x-www-form-urlencoded') + assert_kind_of Net::HTTPResponse, res + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_equal data.size, res['content-length'].to_i + end + assert_kind_of String, res.body + assert_equal data, res.body + end + def _test_send_request__HEAD(http) res = http.send_request('HEAD', '/') assert_kind_of Net::HTTPResponse, res diff --git a/test/net/http/utils.rb b/test/net/http/utils.rb index f499963d418729..3618a92b774535 100644 --- a/test/net/http/utils.rb +++ b/test/net/http/utils.rb @@ -282,6 +282,8 @@ def spawn_server handle_post(path, headers, socket) when 'PATCH' handle_patch(path, headers, socket) + when 'QUERY' + handle_query(path, headers, socket) else socket.print "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n" end @@ -330,6 +332,13 @@ def handle_patch(path, headers, socket) socket.print(response) end + def handle_query(path, headers, socket) + body = socket.read(headers['Content-Length'].to_i) + content_type = headers['Content-Type'] || 'application/octet-stream' + response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}" + socket.print(response) + end + def parse_content_type(content_type) return [nil, nil] unless content_type type, *params = content_type.split(';').map(&:strip) diff --git a/test/ruby/test_thread.rb b/test/ruby/test_thread.rb index 475bdc7558cdfe..9f509cc4f220c1 100644 --- a/test/ruby/test_thread.rb +++ b/test/ruby/test_thread.rb @@ -847,6 +847,40 @@ def test_handle_interrupt_blocking assert_equal(:ok, r) end + def test_handle_interrupt_masks_sigint + if /mswin|mingw/ =~ RUBY_PLATFORM + omit "SIGINT handling differs on Windows" + end + + assert_in_out_err([], <<-INPUT, %w(outer false), []) + waiting = Thread::Queue.new + release = Thread::Queue.new + inner = false + + Thread.new do + waiting.pop + Process.kill(:INT, Process.pid) + release.push(true) + end + + begin + Thread.handle_interrupt(SignalException => :never) do + begin + waiting.push(true) + release.pop + rescue Interrupt + inner = true + raise + end + end + rescue Interrupt + puts "outer" + end + + puts inner + INPUT + end + def test_handle_interrupt_and_io assert_in_out_err([], <<-INPUT, %w(ok), []) th_waiting = true diff --git a/thread.c b/thread.c index 72839418fc5074..3cfbe6beff199c 100644 --- a/thread.c +++ b/thread.c @@ -2812,6 +2812,29 @@ rb_threadptr_signal_raise(rb_thread_t *th, int sig) rb_threadptr_raise(th->vm->ractor.main_thread, 2, argv); } +void +rb_threadptr_interrupt_raise(rb_thread_t *th) +{ + rb_thread_t *target_th = th->vm->ractor.main_thread; + + if (rb_threadptr_dead(target_th)) { + return; + } + + /* Preserve the traditional no-message Interrupt from default SIGINT. */ + VALUE exc = rb_exc_new(rb_eInterrupt, 0, 0); + + /* making an exception object can switch thread, + so we need to check thread deadness again */ + if (rb_threadptr_dead(target_th)) { + return; + } + + rb_ec_setup_exception(GET_EC(), exc, Qundef); + rb_threadptr_pending_interrupt_enque(target_th, exc); + rb_threadptr_interrupt(target_th); +} + void rb_threadptr_signal_exit(rb_thread_t *th) { diff --git a/thread_pthread.c b/thread_pthread.c index 1b664afbb1456a..1dc68318495f20 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -709,8 +709,7 @@ thread_sched_set_running(struct rb_thread_sched *sched, rb_thread_t *th) VM_ASSERT(sched->running != th); if (RUBY_DTRACE_RTS_SET_RUNNING_ENABLED()) { - rb_thread_t *old = sched->running; - RUBY_DTRACE_RTS_SET_RUNNING(sched, old, th); + RUBY_DTRACE_RTS_SET_RUNNING(sched, sched->running, th); } sched->running = th; diff --git a/tool/redmine-backporter.rb b/tool/redmine-backporter.rb index 95a9688cb2cef9..bd4a4114354cd0 100755 --- a/tool/redmine-backporter.rb +++ b/tool/redmine-backporter.rb @@ -33,6 +33,7 @@ BACKPORT_CF_KEY = 'cf_5' STATUS_CLOSE = 5 REDMINE_API_KEY = api_key || ENV['REDMINE_API_KEY'] || (puts opts.help; raise 'need to specify REDMINE_API_KEY') +REDMINE_API_KEY_HEADER = {'X-Redmine-API-Key' => REDMINE_API_KEY}.freeze REDMINE_BASE = 'https://bugs.ruby-lang.org' @query = { @@ -164,8 +165,16 @@ def has_commit(commit, branch) system("git", *base, "merge-base", "--is-ancestor", commit, branch) end +def redmine_read_json(uri) + JSON(uri.read($openuri_options.merge(REDMINE_API_KEY_HEADER))) +end + +def redmine_get(http, uri) + http.get(uri.respond_to?(:request_uri) ? uri.request_uri : uri, REDMINE_API_KEY_HEADER) +end + def show_last_journal(http, uri) - res = http.get("#{uri.path}?include=journals") + res = redmine_get(http, "#{uri.path}?include=journals") res.value h = JSON(res.body) x = h["issue"] @@ -219,7 +228,7 @@ class CommandSyntaxError < RuntimeError; end raise CommandSyntaxError unless /\A(\d+)?\z/ =~ args uri = URI(REDMINE_BASE+'/projects/ruby-master/issues.json?'+URI.encode_www_form(@query.dup.merge('page' => ($1 ? $1.to_i : 1)))) # puts uri - res = JSON(uri.read($openuri_options)) + res = redmine_read_json(uri) @issues = issues = res["issues"] from = res["offset"] + 1 total = res["total_count"] @@ -244,7 +253,7 @@ class CommandSyntaxError < RuntimeError; end end uri = "#{REDMINE_BASE}/issues/#{id}" uri = URI(uri+".json?include=children,attachments,relations,changesets,journals") - res = JSON(uri.read($openuri_options)) + res = redmine_read_json(uri) i = res["issue"] unless i["changesets"] abort "You don't have view_changesets permission" @@ -375,7 +384,8 @@ class << @changesets uri = URI("#{REDMINE_BASE}/issues/#{@issue}.json") Net::HTTP.start(uri.host, uri.port, http_options) do |http| - res = http.get(uri.path) + res = redmine_get(http, uri) + res.value data = JSON(res.body) h = data["issue"]["custom_fields"].find{|x|x["id"]==5} if h and val = h["value"] and val != "" diff --git a/vm.c b/vm.c index d0f944592ae55a..58eb41d9113acb 100644 --- a/vm.c +++ b/vm.c @@ -3290,7 +3290,6 @@ rb_vm_update_references(void *ptr) rb_vm_t *vm = ptr; vm->self = rb_gc_location(vm->self); - vm->mark_object_ary = rb_gc_location(vm->mark_object_ary); vm->orig_progname = rb_gc_location(vm->orig_progname); vm->cc_refinement_set = rb_gc_location(vm->cc_refinement_set); @@ -3376,7 +3375,14 @@ rb_vm_mark(void *ptr) rb_box_entry_mark(vm->main_box); } - rb_gc_mark_movable(vm->mark_object_ary); + /* The main Ractor's registered mark objects (rb_gc_register_mark_object) + * are process-lifetime pins. Mark them here as well as from ractor_mark + * so they stay live before the main Ractor joins vm->ractor.set, e.g. + * during early boot under GC.stress. */ + if (vm->ractor.main_ractor && vm->ractor.main_ractor->mark_object_ary) { + rb_gc_mark_movable(vm->ractor.main_ractor->mark_object_ary); + } + rb_gc_mark_movable(vm->orig_progname); rb_gc_mark_movable(vm->coverages); rb_gc_mark_movable(vm->me2counter); @@ -4824,15 +4830,37 @@ rb_vm_register_global_object(VALUE obj) break; } RB_VM_LOCKING() { - VALUE list = GET_VM()->mark_object_ary; + rb_ractor_t *cr = GET_RACTOR(); + if (!cr->mark_object_ary) cr->mark_object_ary = pin_array_list_new(Qnil); + VALUE list = cr->mark_object_ary; VALUE head = pin_array_list_append(list, obj); if (head != list) { - GET_VM()->mark_object_ary = head; + cr->mark_object_ary = head; } RB_GC_GUARD(obj); } } +/* Hand src's registered mark objects to dst (used when a Ractor terminates: + * these are process-lifetime pins, so the main Ractor keeps them alive). */ +void +rb_vm_ractor_migrate_mark_objects(rb_ractor_t *dst, rb_ractor_t *src) +{ + ASSERT_vm_locking(); + VALUE list = src->mark_object_ary; + while (!NIL_P(list) && list) { + struct pin_array_list *array_list; + TypedData_Get_Struct(list, struct pin_array_list, &pin_array_list_type, array_list); + for (long i = 0; i < array_list->len; i++) { + if (!dst->mark_object_ary) dst->mark_object_ary = pin_array_list_new(Qnil); + VALUE head = pin_array_list_append(dst->mark_object_ary, array_list->array[i]); + if (head != dst->mark_object_ary) dst->mark_object_ary = head; + } + list = array_list->next; + } + src->mark_object_ary = 0; +} + VALUE rb_cc_refinement_set_create(void); void @@ -4840,8 +4868,8 @@ Init_vm_objects(void) { rb_vm_t *vm = GET_VM(); - /* initialize mark object array, hash */ - vm->mark_object_ary = pin_array_list_new(Qnil); + /* mark object arrays are per-Ractor (rb_ractor_t.mark_object_ary), + * lazily created on first rb_gc_register_mark_object */ st_init_existing_table_with_size(&vm->ci_table, &vm_ci_hashtype, 0); vm->cc_refinement_set = rb_cc_refinement_set_create(); } diff --git a/vm_core.h b/vm_core.h index c72933ae21f1e5..fba04b7f56ac5c 100644 --- a/vm_core.h +++ b/vm_core.h @@ -679,6 +679,16 @@ typedef struct rb_hook_list_struct { // see builtin.h for definition typedef const struct rb_builtin_function *RB_BUILTIN; +/* The mark redirect used by the object-traversal APIs + * (rb_objspace_reachable_objects_from etc.). It is installed while a traversal + * runs and is NULL during a real GC. Storage is per-Ractor + * (rb_ractor_t.mark_func_data), except on a modular GC where it lives in the VM + * (rb_vm_struct's gc sub-struct; see gc.c). */ +struct gc_mark_func_data_struct { + void *data; + void (*mark_func)(VALUE v, void *data); +}; + typedef struct rb_vm_struct { VALUE self; @@ -763,7 +773,6 @@ typedef struct rb_vm_struct { unsigned int thread_ignore_deadlock: 1; /* object management */ - VALUE mark_object_ary; VALUE **global_object_list; size_t global_object_list_size; size_t global_object_list_capa; @@ -799,10 +808,13 @@ typedef struct rb_vm_struct { struct { struct rb_objspace *objspace; - struct gc_mark_func_data_struct { - void *data; - void (*mark_func)(VALUE v, void *data); - } *mark_func_data; +#if USE_MODULAR_GC + /* A modular GC (e.g. MMTk) may mark on worker threads that have no + * current EC, so the traversal mark redirect must be reachable without + * a Ractor and lives here. Otherwise it is per-Ractor + * (rb_ractor_t.mark_func_data). */ + struct gc_mark_func_data_struct *mark_func_data; +#endif } gc; rb_at_exit_list *at_exit; @@ -2254,6 +2266,7 @@ int rb_signal_buff_size(void); int rb_signal_exec(rb_thread_t *th, int sig); void rb_threadptr_check_signal(rb_thread_t *mth); void rb_threadptr_signal_raise(rb_thread_t *th, int sig); +void rb_threadptr_interrupt_raise(rb_thread_t *th); void rb_threadptr_signal_exit(rb_thread_t *th); int rb_threadptr_execute_interrupts(rb_thread_t *, int); void rb_threadptr_interrupt(rb_thread_t *th); diff --git a/win32/rm.bat b/win32/rm.bat index c41ebfa5ee0177..8d5aaba14734df 100755 --- a/win32/rm.bat +++ b/win32/rm.bat @@ -48,17 +48,20 @@ if "%sub:\=%" == "%sub%" goto :remove_plain goto :remove_end :remove_plain set p=%2%1 - if not exist "%1" goto :remove_end + if not exist "%p%" goto :remove_end if not "%dryrun%" == "" ( echo Removing %p:\=/% goto :remove_end ) ::- Try `rd` first for symlink to a directory; `del` attemps to remove all ::- files under the target directory, instead of the symlink itself. - (rd /q "%p%" || del /q "%p%") 2> nul && goto :remove_end + rd /q "%p%" 2> nul && goto :remove_end + ::- `del` exits with 0 even when nothing matched, so its result cannot + ::- tell whether directories remain; remove them first. if "%recursive%" == "-r" for /D %%I in (%p%) do ( rd /s /q %%I || call set error=%%ERRORLEVEL%% ) + if exist "%p%" del /q "%p%" 2> nul :remove_end endlocal & set "error=%error%" & goto :EOF diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 1cd4c6c70df8a3..1f8b320c4d6749 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -11429,9 +11429,8 @@ mod tests { asm.stack_push(Type::Flonum); asm.stack_push(Type::CString); - let mut value_array: [u64; 2] = [0, 3]; - let pc: *mut VALUE = &mut value_array as *mut u64 as *mut VALUE; - jit.pc = pc; + let mut fake_encoded_iseq: [VALUE; 2] = [VALUE(0), VALUE(3)]; + jit.pc = &mut fake_encoded_iseq[0]; let mut status = gen_opt_reverse(&mut jit, &mut asm); @@ -11442,8 +11441,9 @@ mod tests { assert_eq!(Type::Fixnum, asm.ctx.get_opnd_type(StackOpnd(0))); // Try again with an even number of elements. + fake_encoded_iseq[1] = VALUE(4); + let _ = fake_encoded_iseq; asm.stack_push(Type::Nil); - value_array[1] = 4; status = gen_opt_reverse(&mut jit, &mut asm); assert_eq!(status, Some(KeepCompiling)); diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 29149bbb044836..8ec0c495bbeb9a 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -989,6 +989,7 @@ pub const ROBJECT_OFFSET_AS_ARY: jit_bindgen_constants = 16; pub const RCLASS_OFFSET_PRIME_FIELDS_OBJ: jit_bindgen_constants = 40; pub const TDATA_OFFSET_FIELDS_OBJ: jit_bindgen_constants = 16; pub const RUBY_OFFSET_RHASH_IFNONE: jit_bindgen_constants = 16; +pub const RUBY_RHASH_AR_TABLE_MAX_SIZE: jit_bindgen_constants = 8; pub const RUBY_OFFSET_RSTRING_LEN: jit_bindgen_constants = 16; pub const RB_SHAPE_FLAG_SHIFT: jit_bindgen_constants = 32; pub const RUBY_OFFSET_EC_CFP: jit_bindgen_constants = 16; diff --git a/zjit.h b/zjit.h index 586cf28a20c598..2bbac39b7c7348 100644 --- a/zjit.h +++ b/zjit.h @@ -4,6 +4,8 @@ // This file contains definitions ZJIT exposes to the CRuby codebase // +#include "shape.h" // for shape_id_t + // ZJIT_STATS controls whether to support runtime counters in the interpreter #ifndef ZJIT_STATS # define ZJIT_STATS (USE_ZJIT && RUBY_DEBUG) @@ -91,6 +93,7 @@ void rb_zjit_invalidate_root_box(void); void rb_zjit_jit_frame_update_references(zjit_jit_frame_t *jit_frame); void rb_zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t *cfp); size_t rb_zjit_hash_new_size(void); +bool rb_zjit_class_allocate_instance_fastpath(VALUE klass, size_t *size_out, shape_id_t *shape_id_out); // Special value for cfp->jit_return that means "this is a C method frame, use // rb_zjit_c_frame as the JITFrame". We don't control the native stack layout diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index cc28a6e05e8ceb..943ae22e30b708 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -112,6 +112,7 @@ fn main() { .allowlist_function("rb_zjit_profile_disable") .allowlist_function("rb_zjit_insn_to_bare_insn") .allowlist_function("rb_zjit_hash_new_size") + .allowlist_function("rb_zjit_class_allocate_instance_fastpath") // For crashing .allowlist_function("rb_bug") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 7f62d83ae09b21..1a4f84d60beb4d 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -615,7 +615,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio } Insn::Const { .. } => panic!("Unexpected Const in gen_insn: {insn}"), Insn::NewArray { elements, state } => gen_new_array(jit, asm, opnds!(elements), &function.frame_state(*state)), - Insn::NewHash { elements, state } => gen_new_hash(jit, asm, function, opnds!(elements), &function.frame_state(*state)), + Insn::NewHash { elements, state } => { + let sym_keys = elements.iter().step_by(2).all(|&key| function.type_of(key).is_subtype(types::Symbol)); + gen_new_hash(jit, asm, function, opnds!(elements), sym_keys, &function.frame_state(*state)) + } Insn::NewRange { low, high, flag, state } => gen_new_range(jit, asm, function, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), Insn::NewRangeFixnum { low, high, flag, state } => gen_new_range_fixnum(asm, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), Insn::ArrayDup { val, state } => gen_array_dup(asm, opnd!(val), &function.frame_state(*state)), @@ -627,7 +630,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::ArrayPop { array, state } => gen_array_pop(asm, opnd!(array), &function.frame_state(*state)), Insn::ArrayLength { array } => gen_array_length(asm, opnd!(array)), Insn::ObjectAlloc { val, state } => gen_object_alloc(jit, asm, function, opnd!(val), &function.frame_state(*state)), - &Insn::ObjectAllocClass { class, state } => gen_object_alloc_class(asm, class, &function.frame_state(state)), + &Insn::ObjectAllocClass { class, state } => gen_object_alloc_class(jit, asm, class, &function.frame_state(state)), Insn::StringCopy { val, chilled, state } => gen_string_copy(asm, opnd!(val), *chilled, &function.frame_state(*state)), Insn::StringConcat { strings, state } => gen_string_concat(jit, asm, function, opnds!(strings), &function.frame_state(*state)), &Insn::StringGetbyte { string, index } => gen_string_getbyte(asm, opnd!(string), opnd!(index)), @@ -771,7 +774,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::LoadField { recv, id, offset, return_type: _, num_bits } => gen_load_field(asm, opnd!(recv), id, offset, num_bits), &Insn::StoreField { recv, id, offset, val, num_bits } => no_output!(gen_store_field(asm, opnd!(recv), id, offset, opnd!(val), num_bits)), &Insn::WriteBarrier { recv, val } => no_output!(gen_write_barrier(jit, asm, opnd!(recv), opnd!(val), function.type_of(val))), - &Insn::IsBlockGiven { lep } => gen_is_block_given(asm, opnd!(lep)), + &Insn::IsBlockGiven { block_handler } => gen_is_block_given(asm, opnd!(block_handler)), Insn::ArrayInclude { elements, target, state } => gen_array_include(jit, asm, function, opnds!(elements), opnd!(target), &function.frame_state(*state)), Insn::ArrayPackBuffer { elements, fmt, buffer, state } => gen_array_pack_buffer(jit, asm, function, opnds!(elements), opnd!(fmt), (*buffer).map(|buffer| opnd!(buffer)), &function.frame_state(*state)), &Insn::DupArrayInclude { ary, target, state } => gen_dup_array_include(jit, asm, function, ary, opnd!(target), &function.frame_state(state)), @@ -837,8 +840,7 @@ fn gen_defined(jit: &JITState, asm: &mut Assembler, function: &Function, op_type } /// Similar to gen_defined for DEFINED_YIELD -fn gen_is_block_given(asm: &mut Assembler, lep: Opnd) -> Opnd { - let block_handler = asm.load(Opnd::mem(64, lep, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL)); +fn gen_is_block_given(asm: &mut Assembler, block_handler: Opnd) -> Opnd { asm.cmp(block_handler, VM_BLOCK_HANDLER_NONE.into()); asm.csel_e(Qfalse.into(), Qtrue.into()) } @@ -1757,8 +1759,7 @@ fn gen_send_iseq_direct( asm_comment!(asm, "switch to new CFP"); let new_cfp = asm.sub(CFP, RUBY_SIZEOF_CONTROL_FRAME.into()); - asm.mov(CFP, new_cfp); - asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.mov(CFP, new_cfp); // will be published at `ec->cfp` after callee's entrypoint let params = unsafe { iseq.params() }; @@ -2285,6 +2286,7 @@ fn gen_new_hash( asm: &mut Assembler, function: &Function, elements: Vec, + sym_keys: bool, state: &FrameState, ) -> lir::Opnd { if elements.is_empty() { @@ -2302,6 +2304,33 @@ fn gen_new_hash( // the redundant store and be reusable for other types. asm.store(Opnd::mem(VALUE_BITS, hash, RUBY_OFFSET_RHASH_IFNONE), Qnil.into()); hash + // TODO: we should use effects_of for this (we would need to add it). + } else if sym_keys { + // Symbols hash and compare without running Ruby and those operations never raise so + // the bulk insert is leaf. + gen_prepare_leaf_call_with_gc(asm, state); + + let num_pairs = elements.len() / 2; + let hash = if num_pairs <= RUBY_RHASH_AR_TABLE_MAX_SIZE as usize { + let alloc_size = unsafe { rb_zjit_hash_new_size() }; + let flags = RUBY_T_HASH as u64; + let klass = unsafe { rb_cHash }; + + let hash = gc_fastpath::gc_fast_path_new_obj(jit, asm, alloc_size, flags, klass, |asm| { + asm_ccall!(asm, rb_hash_new_with_size, num_pairs.into()) + }); + // TODO: this runs on the slow path too, where rb_hash_new already set + // ifnone. A fast-path-only init hook in gc_fast_path_new_obj would avoid + // the redundant store and be reusable for other types. + asm.store(Opnd::mem(VALUE_BITS, hash, RUBY_OFFSET_RHASH_IFNONE), Qnil.into()); + hash + } else { + asm_ccall!(asm, rb_hash_new_with_size, num_pairs.into()) + }; + + let argv = gen_push_opnds(jit, asm, &elements); + asm_ccall!(asm, rb_hash_bulk_insert, elements.len().into(), argv, hash); + hash } else { gen_prepare_non_leaf_call(jit, asm, function, state); @@ -2344,13 +2373,24 @@ fn gen_object_alloc(jit: &JITState, asm: &mut Assembler, function: &Function, va asm_ccall!(asm, rb_obj_alloc, val) } -fn gen_object_alloc_class(asm: &mut Assembler, class: VALUE, state: &FrameState) -> lir::Opnd { +fn gen_object_alloc_class(jit: &mut JITState, asm: &mut Assembler, class: VALUE, state: &FrameState) -> lir::Opnd { // Allocating an object for a known class with default allocator is leaf; see doc for // `ObjectAllocClass`. gen_prepare_leaf_call_with_gc(asm, state); if unsafe { rb_zjit_class_has_default_allocator(class) } { - // TODO(max): inline code to allocate an instance - asm_ccall!(asm, rb_class_allocate_instance, class.into()) + let mut alloc_size: usize = 0; + let mut shape_id: shape_id_t = 0; + let has_fastpath = unsafe { + rb_zjit_class_allocate_instance_fastpath(class, &mut alloc_size, &mut shape_id) + }; + if has_fastpath { + let flags = (RUBY_T_OBJECT as u64) | ((shape_id as u64) << RB_SHAPE_FLAG_SHIFT as u64); + gc_fastpath::gc_fast_path_new_obj(jit, asm, alloc_size, flags, class, |asm| { + asm_ccall!(asm, rb_class_allocate_instance, class.into()) + }) + } else { + asm_ccall!(asm, rb_class_allocate_instance, class.into()) + } } else { assert!(class_has_leaf_allocator(class), "class passed to ObjectAllocClass must have a leaf allocator"); let alloc_func = unsafe { rb_zjit_class_get_alloc_func(class) }; @@ -2391,6 +2431,11 @@ fn gen_entry_point(jit: &mut JITState, asm: &mut Assembler, jit_entry_idx: Optio let jit_frame = JITFrame::new_iseq(entry_pc(jit.iseq(), jit_entry_idx), jit.iseq(), 0); asm.mov(Opnd::mem(64, NATIVE_BASE_PTR, -SIZEOF_VALUE_I32), Opnd::const_ptr(jit_frame)); asm.mov(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_JIT_RETURN), NATIVE_BASE_PTR); + + // Direct JIT-to-JIT callers switch the CFP register before calling this entry + // point, but they leave ec->cfp pointing at the caller until cfp->jit_return + // is valid so signal-based frame walkers never observe a half-published callee. + asm.mov(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); } /// Compile code that exits from JIT code with a return value @@ -3374,6 +3419,8 @@ c_callable! { unsafe { rb_set_cfp_pc(cfp, pc) }; unsafe { (*cfp)._iseq = iseq }; unsafe { (*cfp).jit_return = std::ptr::null_mut() }; + let ec_cfp = unsafe { ec.byte_add(RUBY_OFFSET_EC_CFP as usize) as *mut CfpPtr }; + unsafe { *ec_cfp = cfp }; } with_vm_lock(src_loc!(), || { diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 51055badf29cad..eed7e280e9a15a 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -2920,6 +2920,96 @@ fn test_new_hash_empty_gc_stress() { "#), @"[Hash, 1, nil, {a: 1}]"); } +// Static-symbol keys hash and compare without running Ruby, so NewHash takes the +// leaf bulk-insert fast path into an inline-allocated ar_table. Runs under GC +// stress to guard the leaf-call preparation. +#[test] +fn test_new_hash_static_sym_keys_gc_stress() { + eval(" + def make(a, b) = {x: a, y: b} + "); + assert_contains_opcode("make", YARVINSN_newhash); + assert_snapshot!(assert_compiles(r#" + begin + GC.stress = true + make(1, 2) + h = make(:foo, [3]) + [h.class, h.size, h[:x], h[:y], h[:z], h.default, h] + ensure + GC.stress = false + end + "#), @"[Hash, 2, :foo, [3], nil, nil, {x: :foo, y: [3]}]"); +} + +// Eight pairs fills an inline embedded ar_table (the fast path); nine crosses +// RHASH_AR_TABLE_MAX_SIZE, so it's built as a pre-sized st_table instead. Both stay +// on the static-symbol leaf path, so this guards the ar_table and st_table routes. +#[test] +fn test_new_hash_static_sym_ar_table_boundary() { + eval(" + def eight(v) = {a:v,b:v,c:v,d:v,e:v,f:v,g:v,h:v} + def nine(v) = {a:v,b:v,c:v,d:v,e:v,f:v,g:v,h:v,i:v} + "); + assert_contains_opcode("eight", YARVINSN_newhash); + assert_contains_opcode("nine", YARVINSN_newhash); + assert_snapshot!(assert_compiles(r#" + begin + GC.stress = true + [eight(7).size, eight(7)[:h], nine([9]).size, nine([9])[:i]] + ensure + GC.stress = false + end + "#), @"[8, 7, 9, [9]]"); +} + +// Dynamic symbol keys hash and compare without running Ruby, so NewHash takes the +// leaf bulk-insert fast path into an inline-allocated ar_table. Runs under GC +// stress to guard the leaf-call preparation. +#[test] +fn test_new_hash_dynamic_sym_keys_gc_stress() { + eval(r#" + def make(k, v) = { :"x_#{k}" => v, :"y_#{k}" => v } + "#); + assert_contains_opcode("make", YARVINSN_newhash); + assert_contains_opcode("make", YARVINSN_intern); + assert_snapshot!(assert_compiles(r#" + begin + GC.stress = true + make("warm", 0) + h = make("k", [3]) + [h.class, h.size, h[:"x_k"], h[:"y_k"]] + ensure + GC.stress = false + end + "#), @r#"[Hash, 2, [3], [3]]"#); +} + +#[test] +fn test_object_alloc_gc_stress() { + eval(" + class Foo + def initialize + @a = 1 + @b = 2 + end + def sum = @a + @b + end + def make = Foo.new + "); + assert_contains_opcode("make", YARVINSN_opt_new); + assert_snapshot!(assert_compiles(r#" + begin + GC.stress = true + make + foo = make + foo.instance_variable_set(:@c, 3) + [foo.class, foo.sum, foo.instance_variables] + ensure + GC.stress = false + end + "#), @"[Foo, 3, [:@a, :@b, :@c]]"); +} + #[test] fn test_new_hash_nonempty() { eval(r#" @@ -4729,6 +4819,46 @@ fn test_profile_frames_from_signal_handler() { assert!(profiler.samples() > 0, "rb_profile_frames was not called from SIGPROF handler"); } +// A direct JIT-to-JIT call switches the CFP register before entering the callee. +// Signal profilers must not observe the callee through ec->cfp until the callee's +// cfp->jit_return points at a valid JITFrame. +#[cfg(all( + any(target_os = "linux", target_os = "macos"), + any(target_arch = "x86_64", target_arch = "aarch64"), +))] +#[test] +fn test_profile_frames_during_direct_jit_to_jit_entry() { + with_inlining_threshold(0, || { + eval(r#" + def profiled_direct_callee(value) + value + 1 + end + + def profiled_direct_loop(n) + i = 0 + value = 0 + while i < n + value = profiled_direct_callee(value) + i += 1 + end + value + end + + # Compile both methods and patch the caller's SendDirect site before + # arming the sampler. + profiled_direct_callee(0) + profiled_direct_callee(0) + profiled_direct_loop(1) + profiled_direct_loop(1) + profiled_direct_loop(1) + "#); + + let profiler = signal_profiler::Profiler::start(10); + assert_snapshot!(assert_compiles("profiled_direct_loop(1_000_000)"), @"1000000"); + assert!(profiler.samples() > 0, "rb_profile_frames was not called from SIGPROF handler"); + }); +} + #[test] fn test_profile_under_nested_jit_call() { assert_snapshot!(inspect(" diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 7b000daa7393cb..6b2c6c0e5f078f 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1958,6 +1958,7 @@ pub const ROBJECT_OFFSET_AS_ARY: jit_bindgen_constants = 16; pub const RCLASS_OFFSET_PRIME_FIELDS_OBJ: jit_bindgen_constants = 40; pub const TDATA_OFFSET_FIELDS_OBJ: jit_bindgen_constants = 16; pub const RUBY_OFFSET_RHASH_IFNONE: jit_bindgen_constants = 16; +pub const RUBY_RHASH_AR_TABLE_MAX_SIZE: jit_bindgen_constants = 8; pub const RUBY_OFFSET_RSTRING_LEN: jit_bindgen_constants = 16; pub const RB_SHAPE_FLAG_SHIFT: jit_bindgen_constants = 32; pub const RUBY_OFFSET_EC_CFP: jit_bindgen_constants = 16; @@ -2234,6 +2235,11 @@ unsafe extern "C" { pub fn rb_iseq_label(iseq: *const rb_iseq_t) -> VALUE; pub fn rb_iseq_defined_string(type_: defined_type) -> VALUE; pub fn rb_zjit_hash_new_size() -> usize; + pub fn rb_zjit_class_allocate_instance_fastpath( + klass: VALUE, + size_out: *mut usize, + shape_id_out: *mut shape_id_t, + ) -> bool; pub fn rb_profile_frames( start: ::std::os::raw::c_int, limit: ::std::os::raw::c_int, diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 29c32fe9aab5e9..4ff3eb759199be 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -352,7 +352,8 @@ fn inline_kernel_block_given_p(fun: &mut hir::Function, block: hir::BlockId, _re // Equivalent of GET_LEP() macro. let level = crate::cruby::get_lvar_level(call_site_iseq); let lep = fun.push_insn(block, hir::Insn::GetEP { level }); - Some(fun.push_insn(block, hir::Insn::IsBlockGiven { lep })) + let block_handler = fun.load_field(block, lep, FieldName::VM_ENV_DATA_INDEX_SPECVAL, SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL, types::RubyValue); + Some(fun.push_insn(block, hir::Insn::IsBlockGiven { block_handler })) } else { Some(fun.push_insn(block, hir::Insn::Const { val: hir::Const::Value(Qfalse) })) } diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 4f1b8750fa00e8..1dd357e76bbc0f 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -992,7 +992,7 @@ pub enum Insn { GetConstantPath { ic: *const iseq_inline_constant_cache, state: InsnId }, /// Kernel#block_given? but without pushing a frame. Similar to [`Insn::Defined`] with /// `DEFINED_YIELD` - IsBlockGiven { lep: InsnId }, + IsBlockGiven { block_handler: InsnId }, /// Test the bit at index of val, a Fixnum. /// Return Qtrue if the bit is set, else Qfalse. FixnumBitCheck { val: InsnId, index: u8 }, @@ -1259,8 +1259,8 @@ macro_rules! for_each_operand_impl { | Insn::IncrCounter(_) | Insn::IncrCounterPtr { .. } => {} - Insn::IsBlockGiven { lep } => { - $visit_one!(*lep); + Insn::IsBlockGiven { block_handler } => { + $visit_one!(*block_handler); } Insn::IsBlockParamModified { flags } => { $visit_one!(*flags); @@ -1686,7 +1686,7 @@ impl Insn { Insn::Defined { .. } => effects::Any, Insn::GetConstant { .. } => effects::Any, Insn::GetConstantPath { .. } => effects::Any, - Insn::IsBlockGiven { .. } => Effect::read_write(abstract_heaps::Other, abstract_heaps::Empty), + Insn::IsBlockGiven { .. } => effects::Empty, Insn::FixnumBitCheck { .. } => effects::Empty, // IsA needs to read the class of the value and traverse the class hierarchy, which we model as reading from Memory. Insn::IsA { .. } => Effect::read_write(abstract_heaps::Memory, abstract_heaps::Empty), @@ -2200,7 +2200,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { write!(f, "GetConstant {klass}, :{}, {allow_nil}", id.contents_lossy()) } Insn::GetConstantPath { ic, .. } => { write!(f, "GetConstantPath {:p}", self.ptr_map.map_ptr(ic)) }, - Insn::IsBlockGiven { lep } => { write!(f, "IsBlockGiven {lep}") }, + Insn::IsBlockGiven { block_handler } => { write!(f, "IsBlockGiven {block_handler}") }, Insn::FixnumBitCheck {val, index} => { write!(f, "FixnumBitCheck {val}, {index}") }, Insn::CCall { cfunc, recv, args, name, owner, return_type: _, elidable: _ } => { let display_name = if *owner == Qnil { name.contents_lossy().to_string() } else { qualified_method_name(*owner, *name) }; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 6db2f1d74e0b99..2a59144b2d6c77 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -3581,9 +3581,10 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Object@0x1000, block_given?@0x1008, cme:0x1010) v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:CPtr = GetEP 0 - v21:BoolExact = IsBlockGiven v20 + v21:RubyValue = LoadField v20, :VM_ENV_DATA_INDEX_SPECVAL@0x1038 + v22:BoolExact = IsBlockGiven v21 CheckInterrupts - Return v21 + Return v22 "); }