- ### Problem
When a Ractor isolation error happens due to setting an ivar on a
class, the message doesn't include the name of the ivar and neither
the name of the class. It makes it difficult to track down where the
isolation problem comes from.
I'd like the message to be similar as what we get when a Ractor
access an unshareable ivar.
This is the current behaviour
```ruby
class Foo
def self.foo
@foo ||= "test"
end
end
Ractor.new do
Foo.foo # can not set instance variables of classes/modules by non-main Ractors
end.join
Foo.foo
Ractor.new do
Foo.foo # can not get unshareable values from instance variables of classes/modules from non-main Ractors (@foo from Foo)
end.join
```
### Solution
Modify the error message to include the ivar name and the class/module name.
Co-authored-by: Yaroslav Markin <yaroslav@markin.net>
There was a condition that only a heap with > 1 page can move an empty
page from free_pages to empty_pages. This was even true during a retire
GC (Ractor#join), so running a retire GC couldn't release these actually
empty pages back to the page pool. This meant that when a terminated Ractor
got absorbed, 768KB (12 heap pages) came with it, even if those pages were
empty (this assumes the Ractor had a page for every heap).
Since Ractors can now have 0 pages and the main global GC trigger is
zombie pages (and not retired Ractors), we need to count retired Ractors
with 0 pages towards this zombie pages limit. For now, the floor is
HEAP_COUNT (12 pages), but this can be changed if it's too high.
Data.new turned positional arguments into a keyword Hash and called
initialize with it. When initialize is the default Data#initialize and
every member is given, that call can only store the members and freeze
the instance, so do that directly. A TracePoint on c_call no longer sees
Data#initialize for such calls.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
obj_traverse_i called rb_ary_cancel_sharing on every array it visited,
before the traversal knew whether the object graph was shareable.
So Ractor.shareable? modified frozen arrays even when it returned false.
This commit moves rb_ary_cancel_sharing into mark_shareable, which
runs only after the traversal succeeds.
[Bug #22460]
4e746b14fd added it only to Makefile.sub, so GNU make builds kept a 32-bit rb_len_t, differing from nmake builds, and failed the static assertion in yjit.c with YJIT enabled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
interpreter
(https://github.com/ruby/syntax_suggest/pull/266)
* Prefer Ruby::Prism, the parser built into the interpreter
Ruby 4.1 exposes the prism parser built into the interpreter as
Ruby::Prism, and the prism gem becomes a bundled gem
(https://bugs.ruby-lang.org/issues/22379).
syntax_suggest is loaded when a script fails to parse, so it should not
depend on the prism gem, which may be absent from the Gemfile under
bundler. On Ruby 4.1+ use Ruby::Prism; on older Rubies keep requiring
the prism gem.
Every reference to Prism in syntax_suggest is lexically inside the
SyntaxSuggest module, so defining SyntaxSuggest::Prism is enough and
the call sites are untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Update lib/syntax_suggest/api.rb
* Update CHANGELOG.md
* Apply suggestion from @Earlopain
---------
https://github.com/ruby/syntax_suggest/commit/e11b8842ff
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Earlopain <14981592+Earlopain@users.noreply.github.com>
Add a new `Primitive.attr :always_inline`. Force inline ISEQs with this
attribute. This is helpful for e.g. `BasicObject#!=`, which is otherwise
megamorphic, and very small.
Other JITs also have a "always inline small methods" policy but I didn't
want to wade into that yet.
Matches Thread#alive? and Fiber#alive?. Returns false once the ractor's
result has been recorded. Reads sync.legacy under the ractor lock, as
Ractor#monitor does, so it cannot race with termination.
[Feature #22441]
Defining a method in a refinement makes rb_method_entry_make (VM lock held)
add an entry to the refined class by calling rb_add_method. rb_add_method must
be called without the VM lock: it asserts that, and emits warnings, which can
run Ruby code, after its critical section. With a second Ractor around that
tripped ASSERT_vm_unlocking and called rb_warn with the lock held (e.g.
refine(Object) { def respond_to? }).
Add the entry with rb_method_entry_make directly and let it store its warnings
in the caller's struct, so the outermost rb_add_method emits them after
unlocking. Every warning is still emitted; the 'object_id' and '__send__'
ones, which used to be emitted twice, now once.
rb_const_remove deleted the entry from the constant table without the VM
lock and then freed it, while a Ractor that does not own the class looks
the table up under the VM lock but reads the entry after releasing it.
Reading a constant while its owner removes it could crash in the table
lookup or return garbage (an Integer, Float or false that was never
assigned).
Delete under the VM lock, and have the readers that may not own the class
copy the entry while holding it (rb_const_lookup_copy). Class#dup and
the singleton class clone walk the original's table under the lock for
the same reason; the Class#dup check collects the candidates under the
lock and runs rb_ractor_shareable_p after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Don't try and guess from profiles; nil check is cheap. Helps Rails activerecord/relation/delegation.rb:
def create(model, ...)
relation_class_for(model).new(model, ...)
end
Depending on what keywords are passed (none, some), the hash in splatkw
can either be nil or a Hash. Just check for both cases. The `to_hash`
case is probably very rare.
Under the M:N thread scheduler, a Ruby thread in a non-main Ractor may be
resumed on a different native thread after it blocks. The C compiler
assumes that the native thread never changes within a function, so it may
compute the address of ruby_current_ec once (`__tls_get_addr` in
libruby.so) and reuse it after the switch. The address then refers to the
TLS of the previous native thread, which leads to a SEGV or to using
another thread's ec.
8.times.map do
Ractor.new do
50.times do
",,".split(/(,)/) { sleep 0.001 }
end
:ok
end
end.map(&:value)
#=> [BUG] Segmentation fault at 0x0000000000000030 (with Clang 21)
arm64 and ppc64le already use the out-of-line rb_current_ec()
(RB_THREAD_CURRENT_EC_NOINLINE), which prevents the compiler from caching
the address. Use it on all platforms, and remove the macro.
[Bug #22453]
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ruby_nt_serial and RUBY_NT_SERIAL were declared only when
RB_THREAD_CURRENT_EC_NOINLINE is not defined, so RUBY_DEBUG_LOG() did not
show the native thread serial on arm64 and ppc64le. ruby_nt_serial is not
related to the way ruby_current_ec is accessed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some example results in the docs no longer match what Ruby prints:
* Array#flatten and Array#flatten!: Set#inspect now shows Set[6, 7]
* Module: Mod.constants also includes Math::DomainError
* Module#const_get and Module#const_set: Floats are shown with full
precision
* Complex#imag: sin(PI/4) is 0.7071067811865475
* String#<<: missing # in #<Encoding:UTF-8>
* Enumerable#inject: typo #=>>
A Ractor's zombies with finalizers are poisoned and moved to the main
objspace when the Ractor terminates. If the process exits before they
are finalized, rb_gc_impl_shutdown_call_finalizer_i touches them and
ASAN reports use-after-poison.
```
8.times.map do
Ractor.new do
10.times do
4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) }
GC.start
sleep 0.0001
end
end
end.each(&:join)
#=> ERROR: AddressSanitizer: use-after-poison (sometimes)
```
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They index the match registers, whose count is an int, rather than a String, so rb_len_t does not apply. Only the length of a group name given as a String stays in rb_len_t.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thread_sched_wait_events() returned thread_sched_wait_event when an
interrupt was already pending at entry, without looking at the fd, and
thread_io_wait() reported that as readiness: under RUBY_MN_THREADS=2,
IO#wait_readable on a pipe nobody wrote to returned truthy when a trap
arrived just before the wait.
A wakeup for a masked interrupt (a trap arriving inside a trap handler)
was reported as a timeout, so wait_readable(0.3) in a trap handler
returned nil at once.
Add thread_sched_wait_interrupted for both "stopped for an interrupt"
cases. thread_io_wait() now runs the whole wait under one tag, handles
the interrupt and waits again on the M:N scheduler until its deadline,
and treats a wakeup without an event before the deadline the same way.
At the deadline it probes the fd once. rb_thread_io_blocking_call()
retries func for every M:N outcome but unhandled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers the previous two commits: wait_readable(0) and
IO.select([io], nil, nil, 0) on an io at EOF must always report it,
also while another thread keeps asking for the GVL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same as the previous commit, for the select(2) path used by IO.select
(and by thread_io_wait without poll). When an interrupt was pending,
select was skipped with result 0, which wait_retryable takes as a
timeout, so IO.select([io], nil, nil, 0) could return nil for an io at
EOF. With another thread looping on Thread.pass, 137-153 of about 10^6
such calls returned nil; 0 with this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wait_for_single_fd_blocking_region skips ppoll when an interrupt is
already pending, but left result 0, which wait_retryable reads as a
timeout. With a zero timeout the deadline has passed, so it does not
retry: IO#wait_readable(0) returns nil even for a socket at EOF. This
happens about once per 10^4 probes whenever another thread is around
(timer interrupt).
Net::HTTP#begin_transport probes a kept-alive connection with
wait_readable(0) && eof?, so it reused a connection the server had
already closed and the next non-idempotent request raised EOFError
(spec/ruby/library/net-http/http/request_spec.rb:71 on trunk_asan).
Report the skip as EINTR so the probe polls again after the interrupt
is handled. The same pattern remains in do_select.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The C++ interface is intentionally minimal: it provides only a
couple of niceties in order to largely stay out of the way. It is
mostly used to provide:
* RAII for opaque resources
* Safe up and down casting for nodes
* string_view for various resources
* Iteration over resource lists
Note that if consumers want to use it, they must be on C++17 at
minimum.
https://github.com/ruby/prism/commit/57b8dbe0e9
`__MINGW_PRINTF_FORMAT` is defined in stdio.h.
```
prism/buffer.c: In function 'pm_buffer_append_format':
prism/buffer.c:114:5: warning: function 'pm_buffer_append_format' might be a candidate for 'gnu_printf' format attribute [-Wsuggest-attribute=format]
114 | int result = vsnprintf(NULL, 0, format, arguments);
| ^~~
prism/buffer.c:123:9: warning: function 'pm_buffer_append_format' might be a candidate for 'gnu_printf' format attribute [-Wsuggest-attribute=format]
123 | vsnprintf(buffer->value + cursor, length, format, arguments);
| ^~~~~~~~~
```
https://github.com/ruby/prism/commit/72da47973e
We do not want strings to have the ability or the contract of
having to be freed. This makes the API simpler and keeps us away
from dynamic allocation.
https://github.com/ruby/prism/commit/c9b06230ec
So that consumers don't unintentionally encode the fact that a
constant id is actually 1 + its index in the constants table.
Ideally this makes it so that we can change the underlying
implementation if we want to.
https://github.com/ruby/prism/commit/3a7f3cb6d5
Returns the binary representation of the address value as a string of
"0" and "1" characters, right adjusted to the address size (32
characters for IPv4, 128 for IPv6).
https://github.com/ruby/ipaddr/commit/63beaa749e