Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a8f269a
ZJIT: Deduplicate successor and predecessor sets (#15263)
aidenfoxivey Nov 20, 2025
0b4420b
[ruby/prism] Use memmove for overlapping memory ranges
stevenjohnstone Nov 20, 2025
ba47c2f
[ruby/prism] Add tests to `regexp_encoding_option_mismatch`
thdaraujo Nov 20, 2025
cb9c7a6
[ruby/rubygems] Improve error messages and handling in tests
eileencodes Nov 19, 2025
59e0489
[DOC] Tweaks for String#upcase (#15244)
BurdetteLamar Nov 20, 2025
a4a99a2
[DOC] TWeaks for String#upcase!
BurdetteLamar Nov 19, 2025
9b87a0b
Fix missing write barrier on namespace classext
jhawthorn Nov 5, 2025
bd60600
[ruby/rubygems] Run git operations in parallel to speed things up:
Edouard-chin Nov 19, 2025
409c004
[ruby/rubygems] Make the Bundler logger thread safe:
Edouard-chin Nov 19, 2025
8b71234
[ruby/rubygems] Change the logger instance for this spec:
Edouard-chin Nov 20, 2025
d1b1159
[DOC] Tweaks for String#upto
BurdetteLamar Nov 19, 2025
ff1d23e
Use a serial to keep track of Mutex-owning Fiber
jhawthorn Nov 5, 2025
826e91a
[DOC] Harmonize mod methods
BurdetteLamar Nov 20, 2025
d5368fc
[DOC] Tweaks for String#valid_encoding?
BurdetteLamar Nov 20, 2025
55938a4
[DOC] Sort some methods in What's Here
BurdetteLamar Nov 20, 2025
2447380
Run rb_gc_before_fork after before_exec
peterzhu2118 Nov 18, 2025
604fc05
ZJIT: Rename array length reference to make the code easier to follow
nirvdrum Nov 13, 2025
b06dd64
ZJIT: Compile the VM_OPT_NEWARRAY_SEND_HASH variant of opt_newarray_send
nirvdrum Nov 14, 2025
7d2f9ab
ZJIT: Handle display formatting for all defined bops
nirvdrum Nov 17, 2025
36f1ab9
ZJIT: Add tests for `opt_newarray_send` with target methods redefined
nirvdrum Nov 20, 2025
447989e
ZJIT: Update test names to use the same convention as the HIR tests
nirvdrum Nov 20, 2025
fb28d47
ZJIT: Change the output on redefined method tests to verify the new d…
nirvdrum Nov 20, 2025
82d8d24
[ruby/rubygems] Add support for lockfile in Gemfile
jeremyevans Nov 9, 2025
1562803
[ruby/rubygems] Add support for bundle install --no-lock
jeremyevans Nov 9, 2025
010b23a
[ruby/rubygems] Add support for BUNDLE_LOCKFILE environment variable
jeremyevans Nov 11, 2025
3ec44a9
Add a Ractor test case that causes MMTk to deadlock
peterzhu2118 Nov 18, 2025
e15b4e1
Bump default compiler to clang-20 in CI
peterzhu2118 Nov 20, 2025
29d8a50
[ruby/rubygems] Keep legacy windows platform, not removed them
hsbt Nov 20, 2025
aa9e15c
Fix multiple bugs in `IO::Buffer.map` and update its documentation. (…
trinistr Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/compilers/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: >-
inputs:
tag:
required: false
default: clang-18
default: clang-20
description: >-
container image tag to use in this run.

Expand Down
20 changes: 20 additions & 0 deletions bootstraptest/test_ractor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2350,3 +2350,23 @@ def call_test(obj)
end
:ok
RUBY

assert_equal 'ok', <<~'RUBY'
begin
100.times do |i|
Ractor.new(i) do |j|
1000.times do |i|
"#{j}-#{i}"
end
Ractor.receive
end
pid = fork { }
_, status = Process.waitpid2 pid
raise unless status.success?
end

:ok
rescue NotImplementedError
:ok
end
RUBY
6 changes: 6 additions & 0 deletions class.c
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ rb_class_set_box_classext(VALUE obj, const rb_box_t *box, rb_classext_t *ext)
};

st_update(RCLASS_CLASSEXT_TBL(obj), (st_data_t)box->box_object, rb_class_set_box_classext_update, (st_data_t)&args);

// FIXME: This is done here because this is the first time the objects in
// the classext are exposed via this class. It's likely that if GC
// compaction occurred between the VALUEs being copied in and this
// writebarrier trigger the values will be stale.
rb_gc_writebarrier_remember(obj);
}

RUBY_EXTERN rb_serial_t ruby_vm_global_cvar_state;
Expand Down
18 changes: 18 additions & 0 deletions cont.c
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ struct rb_fiber_struct {

unsigned int killed : 1;

rb_serial_t serial;

struct coroutine_context context;
struct fiber_pool_stack stack;
};
Expand Down Expand Up @@ -1010,6 +1012,13 @@ rb_fiber_threadptr(const rb_fiber_t *fiber)
return fiber->cont.saved_ec.thread_ptr;
}

rb_serial_t
rb_fiber_serial(const rb_fiber_t *fiber)
{
VM_ASSERT(fiber->serial >= 1);
return fiber->serial;
}

static VALUE
cont_thread_value(const rb_context_t *cont)
{
Expand Down Expand Up @@ -1995,6 +2004,13 @@ fiber_alloc(VALUE klass)
return TypedData_Wrap_Struct(klass, &fiber_data_type, 0);
}

static rb_serial_t
next_fiber_serial(void)
{
static rbimpl_atomic_uint64_t fiber_serial = 1;
return (rb_serial_t)ATOMIC_U64_FETCH_ADD(fiber_serial, 1);
}

static rb_fiber_t*
fiber_t_alloc(VALUE fiber_value, unsigned int blocking)
{
Expand All @@ -2011,6 +2027,7 @@ fiber_t_alloc(VALUE fiber_value, unsigned int blocking)
fiber->cont.type = FIBER_CONTEXT;
fiber->blocking = blocking;
fiber->killed = 0;
fiber->serial = next_fiber_serial();
cont_init(&fiber->cont, th);

fiber->cont.saved_ec.fiber_ptr = fiber;
Expand Down Expand Up @@ -2563,6 +2580,7 @@ rb_threadptr_root_fiber_setup(rb_thread_t *th)
fiber->cont.saved_ec.thread_ptr = th;
fiber->blocking = 1;
fiber->killed = 0;
fiber->serial = next_fiber_serial();
fiber_status_set(fiber, FIBER_RESUMED); /* skip CREATED */
th->ec = &fiber->cont.saved_ec;
cont_init_jit_cont(&fiber->cont);
Expand Down
4 changes: 2 additions & 2 deletions doc/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,10 @@
#
# _Counts_
#
# - #length (aliased as #size): Returns the count of characters (not bytes).
# - #empty?: Returns whether the length of +self+ is zero.
# - #bytesize: Returns the count of bytes.
# - #count: Returns the count of substrings matching given strings.
# - #empty?: Returns whether the length of +self+ is zero.
# - #length (aliased as #size): Returns the count of characters (not bytes).
#
# _Substrings_
#
Expand Down
20 changes: 20 additions & 0 deletions doc/string/upcase.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Returns a new string containing the upcased characters in +self+:

'Hello, World!'.upcase # => "HELLO, WORLD!"

The sizes of +self+ and the upcased result may differ:

s = 'Straße'
s.size # => 6
s.upcase # => "STRASSE"
s.upcase.size # => 7

Some characters (and some character sets) do not have upcased and downcased versions:

s = 'よろしくお願いします'
s.upcase == s # => true

The casing may be affected by the given +mapping+;
see {Case Mapping}[rdoc-ref:case_mapping.rdoc].

Related: see {Converting to New String}[rdoc-ref:String@Converting+to+New+String].
38 changes: 38 additions & 0 deletions doc/string/upto.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
With a block given, calls the block with each +String+ value
returned by successive calls to String#succ;
the first value is +self+, the next is <tt>self.succ</tt>, and so on;
the sequence terminates when value +other_string+ is reached;
returns +self+:

a = []
'a'.upto('f') {|c| a.push(c) }
a # => ["a", "b", "c", "d", "e", "f"]

a = []
'Ж'.upto('П') {|c| a.push(c) }
a # => ["Ж", "З", "И", "Й", "К", "Л", "М", "Н", "О", "П"]

a = []
'よ'.upto('ろ') {|c| a.push(c) }
a # => ["よ", "ら", "り", "る", "れ", "ろ"]

a = []
'a8'.upto('b6') {|c| a.push(c) }
a # => ["a8", "a9", "b0", "b1", "b2", "b3", "b4", "b5", "b6"]

If argument +exclusive+ is given as a truthy object, the last value is omitted:

a = []
'a'.upto('f', true) {|c| a.push(c) }
a # => ["a", "b", "c", "d", "e"]

If +other_string+ would not be reached, does not call the block:

'25'.upto('5') {|s| fail s }
'aa'.upto('a') {|s| fail s }

With no block given, returns a new Enumerator:

'a8'.upto('b6') # => #<Enumerator: "a8":upto("b6")>

Related: see {Iterating}[rdoc-ref:String@Iterating].
8 changes: 8 additions & 0 deletions doc/string/valid_encoding_p.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Returns whether +self+ is encoded correctly:

s = 'Straße'
s.valid_encoding? # => true
s.encoding # => #<Encoding:UTF-8>
s.force_encoding(Encoding::ASCII).valid_encoding? # => false

Related: see {Querying}[rdoc-ref:String@Querying].
1 change: 1 addition & 0 deletions internal/cont.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ VALUE rb_fiber_inherit_storage(struct rb_execution_context_struct *ec, struct rb
VALUE rb_fiberptr_self(struct rb_fiber_struct *fiber);
unsigned int rb_fiberptr_blocking(struct rb_fiber_struct *fiber);
struct rb_execution_context_struct * rb_fiberptr_get_ec(struct rb_fiber_struct *fiber);
rb_serial_t rb_fiber_serial(const struct rb_fiber_struct *fiber);

#endif /* INTERNAL_CONT_H */
64 changes: 44 additions & 20 deletions io_buffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -667,26 +667,33 @@ rb_io_buffer_map(VALUE io, size_t size, rb_off_t offset, enum rb_io_buffer_flags
* call-seq: IO::Buffer.map(file, [size, [offset, [flags]]]) -> io_buffer
*
* Create an IO::Buffer for reading from +file+ by memory-mapping the file.
* +file_io+ should be a +File+ instance, opened for reading.
* +file+ should be a +File+ instance, opened for reading or reading and writing.
*
* Optional +size+ and +offset+ of mapping can be specified.
* Trying to map an empty file or specify +size+ of 0 will raise an error.
* Valid values for +offset+ are system-dependent.
*
* By default, the buffer would be immutable (read only); to create a writable
* mapping, you need to open a file in read-write mode, and explicitly pass
* +flags+ argument without IO::Buffer::IMMUTABLE.
* By default, the buffer is writable and expects the file to be writable.
* It is also shared, so several processes can use the same mapping.
*
* You can pass IO::Buffer::READONLY in +flags+ argument to make a read-only buffer;
* this allows to work with files opened only for reading.
* Specifying IO::Buffer::PRIVATE in +flags+ creates a private mapping,
* which will not impact other processes or the underlying file.
* It also allows updating a buffer created from a read-only file.
*
* File.write('test.txt', 'test')
*
* buffer = IO::Buffer.map(File.open('test.txt'), nil, 0, IO::Buffer::READONLY)
* # => #<IO::Buffer 0x00000001014a0000+4 MAPPED READONLY>
* # => #<IO::Buffer 0x00000001014a0000+4 EXTERNAL MAPPED FILE SHARED READONLY>
*
* buffer.readonly? # => true
*
* buffer.get_string
* # => "test"
*
* buffer.set_string('b', 0)
* # `set_string': Buffer is not writable! (IO::Buffer::AccessError)
* # 'IO::Buffer#set_string': Buffer is not writable! (IO::Buffer::AccessError)
*
* # create read/write mapping: length 4 bytes, offset 0, flags 0
* buffer = IO::Buffer.map(File.open('test.txt', 'r+'), 4, 0)
Expand All @@ -708,31 +715,48 @@ io_buffer_map(int argc, VALUE *argv, VALUE klass)
// We might like to handle a string path?
VALUE io = argv[0];

rb_off_t file_size = rb_file_size(io);
// Compiler can confirm that we handled file_size <= 0 case:
if (UNLIKELY(file_size <= 0)) {
rb_raise(rb_eArgError, "Invalid negative or zero file size!");
}
// Here, we assume that file_size is positive:
else if (UNLIKELY((uintmax_t)file_size > SIZE_MAX)) {
rb_raise(rb_eArgError, "File larger than address space!");
}

size_t size;
if (argc >= 2 && !RB_NIL_P(argv[1])) {
size = io_buffer_extract_size(argv[1]);
}
else {
rb_off_t file_size = rb_file_size(io);

// Compiler can confirm that we handled file_size < 0 case:
if (file_size < 0) {
rb_raise(rb_eArgError, "Invalid negative file size!");
if (UNLIKELY(size == 0)) {
rb_raise(rb_eArgError, "Size can't be zero!");
}
// Here, we assume that file_size is positive:
else if ((uintmax_t)file_size > SIZE_MAX) {
rb_raise(rb_eArgError, "File larger than address space!");
}
else {
// This conversion should be safe:
size = (size_t)file_size;
if (UNLIKELY(size > (size_t)file_size)) {
rb_raise(rb_eArgError, "Size can't be larger than file size!");
}
}
else {
// This conversion should be safe:
size = (size_t)file_size;
}

// This is the file offset, not the buffer offset:
rb_off_t offset = 0;
if (argc >= 3) {
offset = NUM2OFFT(argv[2]);
if (UNLIKELY(offset < 0)) {
rb_raise(rb_eArgError, "Offset can't be negative!");
}
if (UNLIKELY(offset >= file_size)) {
rb_raise(rb_eArgError, "Offset too large!");
}
if (RB_NIL_P(argv[1])) {
// Decrease size if it's set from the actual file size:
size = (size_t)(file_size - offset);
}
else if (UNLIKELY((size_t)(file_size - offset) < size)) {
rb_raise(rb_eArgError, "Offset too large!");
}
}

enum rb_io_buffer_flags flags = 0;
Expand Down
1 change: 1 addition & 0 deletions lib/bundler/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ def remove(*gems)
method_option "local", type: :boolean, banner: "Do not attempt to fetch gems remotely and use the gem cache instead"
method_option "prefer-local", type: :boolean, banner: "Only attempt to fetch gems remotely if not present locally, even if newer versions are available remotely"
method_option "no-cache", type: :boolean, banner: "Don't update the existing gem cache."
method_option "no-lock", type: :boolean, banner: "Don't create a lockfile."
method_option "force", type: :boolean, aliases: "--redownload", banner: "Force reinstalling every gem, even if already installed"
method_option "no-prune", type: :boolean, banner: "Don't remove stale gems from the cache (removed)."
method_option "path", type: :string, banner: "Specify a different path than the system default, namely, $BUNDLE_PATH or $GEM_HOME (removed)."
Expand Down
1 change: 1 addition & 0 deletions lib/bundler/cli/install.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def run
# (rather than some optimizations we perform at app runtime).
definition = Bundler.definition(strict: true)
definition.validate_runtime!
definition.lockfile = false if options["no-lock"]

installer = Installer.install(Bundler.root, definition, options)

Expand Down
21 changes: 20 additions & 1 deletion lib/bundler/definition.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require_relative "lockfile_parser"
require_relative "worker"

module Bundler
class Definition
Expand All @@ -9,6 +10,8 @@ class << self
attr_accessor :no_lock
end

attr_writer :lockfile

attr_reader(
:dependencies,
:locked_checksums,
Expand Down Expand Up @@ -379,7 +382,7 @@ def lock(file_or_preserve_unknown_sections = false, preserve_unknown_sections_or
end

def write_lock(file, preserve_unknown_sections)
return if Definition.no_lock || file.nil?
return if Definition.no_lock || !lockfile || file.nil?

contents = to_lock

Expand Down Expand Up @@ -1100,7 +1103,23 @@ def source_requirements
@source_requirements ||= find_source_requirements
end

def preload_git_source_worker
@preload_git_source_worker ||= Bundler::Worker.new(5, "Git source preloading", ->(source, _) { source.specs })
end

def preload_git_sources
sources.git_sources.each {|source| preload_git_source_worker.enq(source) }
ensure
preload_git_source_worker.stop
end

def find_source_requirements
if Gem.ruby_version >= "3.3"
# Ruby 3.2 has a bug that incorrectly triggers a circular dependency warning. This version will continue to
# fetch git repositories one by one.
preload_git_sources
end

# Record the specs available in each gem's source, so that those
# specs will be available later when the resolver knows where to
# look for that gemspec (or its dependencies)
Expand Down
Loading