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 #=>>
Closes: https://github.com/Shopify/ruby/issues/884
Inline `BasicObject#!=` by using a ZJIT hook. This expands specialization coverage to additional cases beyond the existing String and Integer specializations.
Although this might add some overhead in the interpreter when ZJIT is enabled, as the `opt_neq` fast path requires the C definition, the benchmarks below show speedups in several cases (e.g. lobsters, railsbench). I ran the benchmarks several times. The speedups varied between runs, but I consistently observed improvements over the previous implementation.
```
zjit-master: ruby 4.1.0dev (2026-10-10T08:21:06Z master 05917fd1e6) +ZJIT stats +PRISM [arm64-darwin25]
zjit-basicobject-hook: ruby 4.1.0dev (2026-10-10T13:30:56Z zjit-basicobject-n.. a6f95cadaa) +ZJIT stats +PRISM [arm64-darwin25]
ruby-bench: 3cc371202311fe2c821a6adb5adfced364f029de
------------- ---------------- -------------------------- ----------------------------- ---------------------------------
bench zjit-master (ms) zjit-basicobject-hook (ms) zjit-basicobject-hook 1st itr zjit-master/zjit-basicobject-hook
liquid-render 44.6 ± 10.2% 43.9 ± 6.3% 0.933 1.014
lobsters 415.1 ± 6.7% 384.1 ± 4.7% 1.070 1.081
railsbench 592.1 ± 5.8% 545.8 ± 2.0% 1.117 1.085
------------- ---------------- -------------------------- ----------------------------- ---------------------------------
```
---
<details>
<summary>Resolved: specialization with statically known receiver types</summary>
Note: Currently, the `==` call inside `BasicObject#!=` uses a profile shared across callers. When that profile is polymorphic, `==` can fall back to a dynamic send even when the receiver's exact type is known in the caller.
As a follow-up, we could improve specialization of `==` inside the inlined `!=` by using receiver types known at the call site, even when the shared profile for `==` is polymorphic.
```rb
2.times do
"x" != nil
1 != nil
end
NULL = Object.new
def test(value) = NULL != value
4.times { test(nil) }
```
```sh
# ./miniruby --zjit-dump-hir --zjit-call-threshold=4 --zjit-num-profiles=4 ../test.rb
```
On master:
```
v30:ObjectExact[VALUE(...)] = Const Value(VALUE(...))
PatchPoint MethodRedefined(Object@..., !=@..., cme:...)
PatchPoint MethodRedefined(Object@..., ==@..., cme:...)
v51:CBool = IsBitNotEqual v30, v11
v52:BoolExact = BoxBool v51
```
On current branch:
```
v30:ObjectExact[VALUE(...)] = Const Value(VALUE(...))
PatchPoint NoSingletonClass(Object@...)
PatchPoint MethodRedefined(Object@..., !=@..., cme:...)
PushInlineFrame :!=, v30 (...), num_args=1
v68:BasicObject = Send v30, :==, v11 # SendFallbackReason: Send: polymorphic fallback
v71:CBool = Test v68
CondBranch v71, bb14(), bb8()
// Removing the initial 2.times block lets the current branch specialize the comparison to IsBitEqual.
```
</details>
These still held String and Array lengths and indices in long, or cast them to long. rb_len_t is still long everywhere, so the generated code does not change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rb_big_pack() and rb_big_unpack() keep `long`, because num_longs counts the words of an `unsigned long` buffer rather than a String or Array length.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
If a class is allocated and has a singleton class, it is incorrectly marked
as initialized which can cause an assertion error when an instance is
allocated.
For example, the following code causes an assertion error:
c = Class.allocate
c.singleton_class
c.new
Crashes with:
vm_method.c:1861: Assertion Failed: rb_get_alloc_func:allocator
ruby 4.1.0dev (2026-10-07T14:31:40Z io-uninitialized-e.. 67f4ea55a3) +PRISM [arm64-darwin27]
-- Crash Report log information --------------------------------------------
See Crash Report log file in one of the following locations:
* ~/Library/Logs/DiagnosticReports
* /Library/Logs/DiagnosticReports
for more details.
Don't forget to include the above Crash Report log file in bug reports.
-- Control frame information -----------------------------------------------
c:0002 p:0017 s:0009 E:000780 l:n b:---- r:0x0 EVAL test.rb:3 [FINISH]
c:0001 p:0000 s:0003 E:000128 l:y b:---- r:0x0 DUMMY [FINISH]
-- Ruby level backtrace information ----------------------------------------
test.rb:3:in '<main>'
-- Threading information ---------------------------------------------------
Total ractor count: 1
Ruby thread count for this ractor: 1
-- C level backtrace information -------------------------------------------
miniruby(rb_assert_failure_detail+0xf0) [0x102b57024] error.c:1268
miniruby(rb_assert_failure_detail) (null):0
miniruby(rb_assert_failure_detail+0x0) [0x102b56f34] error.c:1244
miniruby(rb_assert_failure) (null):0
miniruby(rb_get_alloc_func.cold.1+0x28) [0x102b663e0] vm_method.c:1861
miniruby(rb_get_alloc_func.cold.3) (null):0
miniruby(rb_type+0x0) [0x1028af338] vm_method.c:1861
miniruby(rb_get_alloc_func) (null):0
miniruby(rb_class_alloc+0x48) [0x102756210] object.c:2285
miniruby(vm_exec_core+0x469c) [0x1028a6114] insns.def:936
miniruby(vm_exec_loop+0x0) [0x10289f25c] vm.c:2909
miniruby(rb_vm_exec) vm.c:2912
miniruby(rb_ec_exec_node+0xa0) [0x1026ac450] eval.c:300
miniruby(ruby_run_node+0x64) [0x1026ac354] eval.c:338
miniruby(rb_main+0x1c) [0x1025e09b0] main.c:42
miniruby(main) main.c:62
This code should not raise an error:
```
@var = Class.new.freeze.dup # unfrozen copy
@var.instance_variable_set(:@test, 123)
```
It was raising `Ractor::IsolationError` because of a bug in `rb_fields_tbl_copy`.
This patch also fixes the following assertion failure in debug mode:
```
Module.new.freeze.singleton_class.instance_variable_set("@a", 1)
```
Now we get the proper behavior:
```
can't modify frozen Class: #<Class:#<Module:0x00000001094d9900>> (FrozenError)
```
Fixes [Bug #22378]
Requested by matz on [Feature #22226]: a non-owner freezing somebody else's
class is a hole, because it lets a third party make a class permanently
unmodifiable. Naming is the same kind of change to the class itself.
Module#freeze checks only when it would actually change something, so an
already frozen class stays a no-op for every Ractor and idempotent .freeze
calls keep working.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only the Ractor which created a class/module may modify it: define, remove or
undefine methods, alias, include/prepend, refine, define or remove constants,
register an autoload, and set the class's own instance variables. Reading is
unchanged. This replaces the assorted "main Ractor only" rules with one
creator-based rule, and in exchange a Ractor gets full use of the classes it
creates itself -- unshareable constant and instance variable values included,
which even the main Ractor could not do for its own classes before.
Three paths reach a class's tables without going through rb_class_modify_check,
so they get the check of their own: Module#refine (which writes its refinement
tables into the receiver as well as into the refined class), Class#initialize
(which writes the superclass of a not-yet-initialized class, and had no frozen
check either), and Module#ruby2_keywords.
The constant inline cache has to move with the rule. Its hit condition was
"shareable value, or we are the main Ractor", which was sound only because a
non-main Ractor could not have an unshareable constant in the first place. Now
it can, so an owner priming the cache would let any other Ractor read that value
straight out of it, never reaching the owner check in vm_get_ev_const. The
entry records the Ractor which filled it instead -- the one that passed the
check -- which also gives a non-main owner a fast path it never had.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Instead of always allocating `ar_table` backed hashes in 160B
slots for them to have capacity of `RHASH_AR_TABLE_MAX_SIZE` (`8`),
they can now be allocated in any slot size between `64B` and `160B`.
`64B` is the minimum slot size that still allows transitioning to
an `st_table` if necessary.
The assumption is that there is a large number of literal hashes
and keyword argument hashes being built, and the overwhelming majority
of them aren't appended to.
When this assumption hold, this patch saves memory and reduce GC
pressure by allocating in a smaller slot than before.
However when the assumption doesn't hold, it has the oppositve effect
as the Hash will transition to an `st_table` when before it may have
stayed an `ar_table`.
Currently the main blocked for this change is that the compiler
has a tendency to generate code that make the final Hash size unknown.
For instance: `{a: 4, b: 4 + 1}` is compiled as:
```
0000 duphash {a: 4} ( 1)[Li]
0002 putspecialobject 1
0004 swap
0005 putobject :b
0007 putobject 4
0009 putobject_INT2FIX_1_
0010 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0012 opt_send_without_block <calldata!mid:core#hash_merge_ptr, argc:3, ARGS_SIMPLE>
```
Here `duphash` has no information about how large of a hash it needs to allocate.
For this patch to be more efficient the compiler should generate something like:
```
0000 putobject {a: 4} ( 1)[Li]
0002 putspecialobject 1
0004 swap
0005 putobject :b
0007 putobject 4
0009 putobject_INT2FIX_1_
0010 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0012 opt_send_without_block <calldata!mid:core#hash_merge_ptr, argc:3, ARGS_SIMPLE>
```
So that it's `hash_merge_ptr` that is responsible for allocating the final hash,
given it does have a much better clue of how large it would need to be.
Demo:
```ruby
require 'objspace'
def size(hash)
puts "size: #{hash.size}\tmemsize: #{ObjectSpace.memsize_of(hash)}"
end
size({})
size(a:1)
size(a:1, b:2)
size(a:1, b:2, c:3)
size(a:1, b:2, c:3, d:4)
size(a:1, b:2, c:3, d:4, e:5)
size(a:1, b:2, c:3, d:4, e:5, f:6)
size(a:1, b:2, c:3, d:4, e:5, f:6, g:7)
size(a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8)
size(a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9)
```
Before:
```
size: 0 memsize: 160
size: 1 memsize: 160
size: 2 memsize: 160
size: 3 memsize: 160
size: 4 memsize: 160
size: 5 memsize: 160
size: 6 memsize: 160
size: 7 memsize: 160
size: 8 memsize: 160
size: 9 memsize: 464
```
After:
```
size: 0 memsize: 64
size: 1 memsize: 64
size: 2 memsize: 64
size: 3 memsize: 80
size: 4 memsize: 96
size: 5 memsize: 128
size: 6 memsize: 128
size: 7 memsize: 160
size: 8 memsize: 160
size: 9 memsize: 448
```
The pattern of allocating an empty hash with `rb_hash_new()` and
then appending to it is relatively wasteful, because every time
we go over the hash capacity, we'll have to `realloc`, rehash etc.
It may even get worse if https://github.com/ruby/ruby/pull/18345
lands, as right now an empty hash starts with a capacity of 8,
but it may very well start with a capacity of 2.
Hence in cases where the final hash size is easy to predict, we
should use `rb_hash_new_capa()`.
Module#descendants returns an array of classes and modules that have
the receiver in their ancestors; it is the inverse of Module#ancestors.
The receiver itself, singleton classes, and refinements are not included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Followup: https://github.com/ruby/ruby/pull/17631
Now that complex `RObject` always have an IMEMO/fields, we no
longer need to have an external `st_table` to replicate the
memory layout of `RObject`.
Instead of spilling into a raw buffer, `RObject` now spills
into an `IMEMO/fields` object like other types.
From an instance variable layout standpoint, an extended `RObject`
is now identical to a `RTypedData`, as in they both store the
reference to their `IMEMO/fields` at the same offset (`VALUE * 2`).
One positive consequence of this is that the only case where a `T_OBJECT`
needs sweeping is if a finalizer was registered.
YJIT now side exit when it need to write a new ivar into a `RObject`
that is out of space. This is unlikely to cause a performance regression
as this codepath isn't supposed to happen after warmup given `max_iv_count`
is recorded on classes, so future objects should be large enough.
Hence it's best not to waste executable memory for such codepath.
ZJIT lost support for writting into extended `RObject`.
Most of the code to support it is there it's just missing an
implementation of `RBASIC_SET_SHAPE_ID`, which recently changed
to strip some bits out of the shape (see comments).
I will leave it to the ZJIT team to implement it (sorry).
There are a few future cleanups planned that I keep for followups:
- We can now get rid of the `ROBJECT_HEAP` flag, it's redundant with
the shape layout bits.
- `imemo_fields` can now embed the `st_table` when complex.
Co-Authored-By: John Hawthorn <john@hawthorn.email>
Co-authored-by: Randy Stauner <randy@r4s6.net>
[Feature #22085]
This warning is only actionable for float literals in source code.
When parsing user input into a float, most of the time this warning
isn't actionable and is pure noise.
This is an often-called internal function that has a fastpath, so the fastpath
should be inlined.
Benchmark:
```ruby
require 'benchmark/ips'
ARR = [1, 2, 3]
Benchmark.ips(quiet: true) do |x|
x.config(time: 2, warmup: 1)
# Control: C method, does not route through rb_check_convert_type_with_id. Detects drift.
x.report("control itself") do |t|
a = ARR
i = 0; while i < t; a.itself; i += 1; end
end
# Target: Array(arr) -> rb_Array -> rb_check_array_type -> inlined RB_TYPE_P fast path
x.report("Array(arr)") do |t|
a = ARR
i = 0; while i < t; Array(a); i += 1; end
end
end
```
When run 10 times, we see a 4% average improvement.
rb_get_freeze_opt and rb_obj_clone_setup lazily initialized shared static
caches (the freeze keyword id and the `{freeze: true/false}` hashes) with a
plain `if (!cache)` guard. When Ractors run #clone concurrently this races:
a thread can publish a half-built hash that another freezes before it is
filled, later triggering "can't modify frozen Hash".
Use the preinterned idFreeze directly, and publish each frozen, pinned hash
with a single atomic CAS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose both `rb_obj_shape_` functions that take a `VALUE`
and `rb_shape_` functions that take a `shape_id`.
Make common transition functions such as `complex` and `frozen`
inlineable.
Also get rid of RB_SET_SHAPE_ID and rb_set_boxed_class_shape_id.
Otherwise some code defining ivars on a naked object early during
program boot can cause all objects to be larger than needed.
This seem like it was always the intention, but wasn't quite
properly prevented.
Also stop updating `max_iv_count` during GC marking as it is
redundant.
Previously, rb_ivar_foreach would walk up the shape tree, but yield
instance variables to the callback as it went. If the object shape was
modified during this callback, particularly with removing an instance
variable, it could result in reading free'd memory.
This commit solves this by adding a version which buffering all instance
variable names and values before calling the callback, giving a snapshot
of the object at the time rb_ivar_foreach_buffered is called.
The buffer is made with ALLOCV_N, so the performance difference should
be minimal, and I don't think this method is particuarly heavily used.
[Bug #21996]
This checks that the value returned from the function registered with
rb_define_alloc_func is of the correct class. When this was first
introduced in 1fe40b7cc5 (by Matz on 2001-10-03), allocation was done
via user-defined Object#allocate, so it made sense to have a runtime
check in release builds. Now that it's defined via rb_define_alloc_func
in the C extension API, I don't think it's necessary.
The check is surprisingly expensive. Removing it makes Object.new about
10% faster. It allows the C compiler to optimize the call to the
function pointer as a tail-call. Removing this also allows ZJIT/YJIT to
call the function directly (ZJIT already does this by having a list of
known safe allocation functions).
There's no way for users to ever have seen this check, other than by
writing a misbehaving C extension, which returns objects with the wrong
class.
[Feature #21966]
Copy the allocator from superclass to subclass at class creation time,
so rb_get_alloc_func no longer needs to walk the ancestor chain.
This expands on 68ffc8db08, which did this
copy, but only for classes defined in Ruby (ex. `class Foo`). And allows
us to simple read the allocator field for any class to get the correct
value.
To keep this correct when rb_define_alloc_func is called after
subclasses already exist, propagate the new allocator down to all
subclasses. An RCLASS_ALLOCATOR_DEFINED flag distinguishes classes with
an explicitly set allocator from those that inherited one, so
propagation stops at subclasses that define their own.