From 3fee7dd90d19790950f476614ae53a95b7730592 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Sat, 20 Dec 2025 15:28:04 -0500 Subject: [PATCH 1/7] Small improvements to doc/language/ractor.md (#15588) [DOC] Improvements to doc/language/ractor.md --- doc/language/ractor.md | 293 +++++++++++++++++++++++------------------ 1 file changed, 165 insertions(+), 128 deletions(-) diff --git a/doc/language/ractor.md b/doc/language/ractor.md index 224e36934b51d2..24b75c1c5b552e 100644 --- a/doc/language/ractor.md +++ b/doc/language/ractor.md @@ -1,39 +1,39 @@ -# Ractor - Ruby's Actor-like concurrent abstraction +# Ractor - Ruby's Actor-like concurrency abstraction -Ractor is designed to provide a parallel execution feature of Ruby without thread-safety concerns. +Ractors are designed to provide parallel execution of Ruby code without thread-safety concerns. ## Summary ### Multiple Ractors in an interpreter process -You can make multiple Ractors and they run in parallel. +You can create multiple Ractors which can run ruby code in parallel. -* `Ractor.new{ expr }` creates a new Ractor and `expr` is run in parallel on a parallel computer. -* Interpreter invokes with the first Ractor (called *main Ractor*). -* If the main Ractor terminates, all other Ractors receive termination requests, similar to how threads behave. (if main thread (first invoked Thread), Ruby interpreter sends all running threads to terminate execution). +* `Ractor.new{ expr }` creates a new Ractor and `expr` can run in parallel with other ractors on a multi-core computer. +* Ruby processes start with one Ractor (called the *main Ractor*). +* If the main Ractor terminates, all other Ractors receive termination requests, similar to how threads behave. * Each Ractor contains one or more Threads. - * Threads within the same Ractor share a Ractor-wide global lock like GIL (GVL in MRI terminology), so they can't run in parallel (without releasing GVL explicitly in C-level). Threads in different ractors run in parallel. - * The overhead of creating a Ractor is similar to overhead of one Thread creation. + * Threads within the same Ractor share a Ractor-wide global lock (GVL in MRI terminology), so they can't run in parallel wich each other (without releasing the GVL explicitly in C extensions). Threads in different ractors can run in parallel. + * The overhead of creating a Ractor is slightly above the overhead of creating a Thread. -### Limited sharing between multiple ractors +### Limited sharing between Ractors -Ractors don't share everything, unlike threads. +Ractors don't share all objects, unlike Threads which can access any object other than objects stored in another Thread's thread-locals. -* Most objects are *Unshareable objects*, so you don't need to care about thread-safety problems which are caused by sharing. -* Some objects are *Shareable objects*. - * Immutable objects: frozen objects which don't refer to unshareable-objects. +* Most objects are *Unshareable objects*. Unshareable objects can only be used by the ractor that instantiated them, so you don't need to worry about thread-safety issues resulting from using the object concurrently across Ractors. +* Some objects are *Shareable objects*. Here is an incomplete list to give you an idea: + * Immutable objects: these are frozen objects which don't refer to unshareable-objects. * `i = 123`: `i` is an immutable object. * `s = "str".freeze`: `s` is an immutable object. - * `a = [1, [2], 3].freeze`: `a` is not an immutable object because `a` refers unshareable-object `[2]` (which is not frozen). - * `h = {c: Object}.freeze`: `h` is an immutable object because `h` refers Symbol `:c` and shareable `Object` class object which is not frozen. - * Class/Module objects + * `a = [1, [2], 3].freeze`: `a` is not an immutable object because `a` refers to the unshareable-object `[2]` (which is not frozen). + * `h = {c: Object}.freeze`: `h` is an immutable object because `h` refers to the Symbol `:c` and the shareable `Object` class. + * Class/Module objects are always shareable, even if they refer to unshareable objects. * Special shareable objects - * Ractor object itself. + * Ractor objects themselves are shareable. * And more... ### Communication between Ractors with `Ractor::Port` -Ractors communicate with each other and synchronize the execution by message exchanging between Ractors. `Ractor::Port` is provided for this communication. +Ractors communicate with each other and synchronize their execution by exchanging messages. The `Ractor::Port` class provides this communication mechanism. ```ruby port = Ractor::Port.new @@ -43,72 +43,65 @@ Ractor.new port do |port| port << 42 end -port.receive # get a message to the port. Only the creator Ractor can receive from the port +port.receive # get a message from the port. Only the ractor that created the Port can receive from it. #=> 42 ``` -Ractors have its own default port and `Ractor#send`, `Ractor.receive` will use it. +All Ractors have a default port, which `Ractor#send`, `Ractor.receive` (etc) will use. -### Copy & Move semantics to send messages +### Copy & Move semantics when sending objects -To send unshareable objects as messages, objects are copied or moved. +To send unshareable objects to another ractor, objects are either copied or moved. -* Copy: use deep-copy. -* Move: move membership. - * Sender can not access the moved object after moving the object. - * Guarantee that at least only 1 Ractor can access the object. +* Copy: deep-copies the object to the other ractor. +* Move: moves membership to another ractor. + * The sending Ractor can not access the moved object after it moves. + * There is a guarantee that only one Ractor can access an unshareable object at once. ### Thread-safety -Ractor helps to write a thread-safe concurrent program, but we can make thread-unsafe programs with Ractors. +Ractors help to write thread-safe, concurrent programs. They allow sharing of data only through explicit message passing for +unshareable objects. Shareable objects are guaranteed to work correctly across ractors, even if the ractors are running in parallel. +This guarantee, however, only applies across ractors. You still need to use Mutexes and other thread-safety tools within a ractor if +you're using multiple ruby Threads. -* GOOD: Sharing limitation - * Most objects are unshareable, so we can't make data-racy and race-conditional programs. - * Shareable objects are protected by an interpreter or locking mechanism. -* BAD: Class/Module can violate this assumption - * To make it compatible with old behavior, classes and modules can introduce data-race and so on. - * Ruby programmers should take care if they modify class/module objects on multi Ractor programs. -* BAD: Ractor can't solve all thread-safety problems - * There are several blocking operations (waiting send) so you can make a program which has dead-lock and live-lock issues. - * Some kind of shareable objects can introduce transactions (STM, for example). However, misusing transactions will generate inconsistent state. - -Without Ractor, we need to trace all state-mutations to debug thread-safety issues. -With Ractor, you can concentrate on suspicious code which are shared with Ractors. + * Most objects are unshareable. You can't create data-races across ractors due to the inability to use these objects across ractors. + * Shareable objects are protected by locks (or otherwise don't need to be) so they can be used by more than one ractor at once. ## Creation and termination ### `Ractor.new` -* `Ractor.new{ expr }` generates another Ractor. +* `Ractor.new{ expr }` creates a Ractor. ```ruby -# Ractor.new with a block creates new Ractor +# Ractor.new with a block creates a new Ractor r = Ractor.new do - # This block will be run in parallel with other ractors + # This block can run in parallel with other ractors end -# You can name a Ractor with `name:` argument. -r = Ractor.new name: 'test-name' do +# You can name a Ractor with a `name:` argument. +r = Ractor.new name: 'my-first-ractor' do end # and Ractor#name returns its name. -r.name #=> 'test-name' +r.name #=> 'my-first-ractor' ``` -### Given block isolation +### Block isolation -The Ractor executes given `expr` in a given block. -Given block will be isolated from outer scope by the `Proc#isolate` method (not exposed yet for Ruby users). To prevent sharing unshareable objects between ractors, block outer-variables, `self` and other information are isolated. +The Ractor executes `expr` in the given block. +The given block will be isolated from its outer scope. To prevent sharing objects between ractors, outer variables, `self` and other information is isolated from the block. -`Proc#isolate` is called at Ractor creation time (when `Ractor.new` is called). If given Proc object is not able to isolate because of outer variables and so on, an error will be raised. +This isolation occurs at Ractor creation time (when `Ractor.new` is called). If the given block is not able to be isolated because of outer variables or `self`, an error will be raised. ```ruby begin a = true r = Ractor.new do - a #=> ArgumentError because this block accesses `a`. + a #=> ArgumentError because this block accesses outer variable `a`. end - r.join # see later + r.join # wait for ractor to finish rescue ArgumentError end ``` @@ -123,7 +116,7 @@ end r.value == self.object_id #=> false ``` -Passed arguments to `Ractor.new()` becomes block parameters for the given block. However, an interpreter does not pass the parameter object references, but send them as messages (see below for details). +Arguments passed to `Ractor.new()` become block parameters for the given block. However, Ruby does not pass the objects themselves, but sends them as messages (see below for details). ```ruby r = Ractor.new 'ok' do |msg| @@ -133,7 +126,7 @@ r.value #=> 'ok' ``` ```ruby -# almost similar to the last example +# similar to the last example r = Ractor.new do msg = Ractor.receive msg @@ -142,9 +135,9 @@ r.send 'ok' r.value #=> 'ok' ``` -### An execution result of given block +### The execution result of the given block -Return value of the given block becomes an outgoing message (see below for details). +The return value of the given block becomes an outgoing message (see below for details). ```ruby r = Ractor.new do @@ -153,11 +146,11 @@ end r.value #=> `ok` ``` -Error in the given block will be propagated to the receiver of an outgoing message. +An error in the given block will be propagated to the consumer of the outgoing message. ```ruby r = Ractor.new do - raise 'ok' # exception will be transferred to the receiver + raise 'ok' # exception will be transferred to the consumer end begin @@ -171,36 +164,39 @@ end ## Communication between Ractors -Communication between Ractors is achieved by sending and receiving messages. There are two ways to communicate with each other. +Communication between Ractors is achieved by sending and receiving messages. There are two ways to communicate: -* (1) Message sending/receiving via `Ractor::Port` +* (1) Sending and receiving messages via `Ractor::Port` * (2) Using shareable container objects * Ractor::TVar gem ([ko1/ractor-tvar](https://github.com/ko1/ractor-tvar)) - * more? -Users can control program execution timing with (1), but should not control with (2) (only manage as critical section). +Users can control program execution timing with (1), but should not control with (2) (only perform critical sections). + +For sending and receiving messages, these are the fundamental APIs: + +* send/receive via `Ractor::Port`. + * `Ractor::Port#send(obj)` (`Ractor::Port#<<(obj)` is an alias) sends a message to the port. Ports are connected to an infinite size incoming queue so it will never block the caller. + * `Ractor::Port#receive` dequeues a message from its own incoming queue. If the incoming queue is empty, `Ractor::Port#receive` will block the execution of the current Thread. + * `Ractor#send` and `Ractor.receive` use ports (their default port) internally, so are conceptually similar to the above. +* You can close a `Ractor::Port` by `Ractor::Port#close`. A port can only be closed by the ractor that created it. + * If a port is closed, you can't `send` to it. Doing so raises an exception. + * When a Ractor is terminated, the Ractor's ports are automatically closed. +* You can wait for a ractor's termination and receive its return value with `Ractor#value`. This is similar to `Thread#value`. -For message sending and receiving, there are two types of APIs: push type and pull type. +There are 3 ways to send an object as a message: -* (1) send/receive via `Ractor::Port`. - * `Ractor::Port#send(obj)` (`Ractor::Port#<<(obj)` is an alias) send a message to the port. Ports are connected to the infinite size incoming queue so `Ractor::Port#send` will never block. - * `Ractor::Port#receive` dequeue a message from its own incoming queue. If the incoming queue is empty, `Ractor::Port#receive` calling will block the execution of a thread. -* `Ractor.select()` can wait for the success of `Ractor::Port#receive`. -* You can close `Ractor::Port` by `Ractor::Port#close` only by the creator Ractor of the port. - * If the port is closed, you can't `send` to the port. If `Ractor::Port#receive` is blocked for the closed port, then it will raise an exception. - * When a Ractor is terminated, the Ractor's ports are closed. -* There are 3 ways to send an object as a message - * (1) Send a reference: Sending a shareable object, send only a reference to the object (fast) - * (2) Copy an object: Sending an unshareable object by copying an object deeply (slow). Note that you can not send an object which does not support deep copy. Some `T_DATA` objects (objects whose class is defined in a C extension, such as `StringIO`) are not supported. - * (3) Move an object: Sending an unshareable object reference with a membership. Sender Ractor can not access moved objects anymore (raise an exception) after moving it. Current implementation makes new object as a moved object for receiver Ractor and copies references of sending object to moved object. `T_DATA` objects are not supported. - * You can choose "Copy" and "Move" by the `move:` keyword, `Ractor#send(obj, move: true/false)` and `Ractor.yield(obj, move: true/false)` (default is `false` (COPY)). +1) Send a reference: sending a shareable object sends only a reference to the object (fast). +2) Copy an object: sending an unshareable object through copying it deeply (can be slow). Note that you can not send an object this way which does not support deep copy. Some `T_DATA` objects (objects whose class is defined in a C extension, such as `StringIO`) are not supported. +3) Move an object: sending an unshareable object across ractors with a membership change. The sending Ractor can not access the moved object after moving it, otherwise an exception will be raised. Implementation note: `T_DATA` objects are not supported. + +You can choose between "Copy" and "Move" by the `move:` keyword, `Ractor#send(obj, move: true/false)`. The default is `false` ("Copy"). However, if the object is shareable it will automatically use `move`. ### Wait for multiple Ractors with `Ractor.select` -You can wait multiple Ractor port's receiving. +You can wait for messages on multiple ports at once. The return value of `Ractor.select()` is `[port, msg]` where `port` is a ready port and `msg` is received message. -To make convenient, `Ractor.select` can also accept Ractors to wait the termination of Ractors. +To make it convenient, `Ractor.select` can also accept Ractors. In this case, it waits for their termination. The return value of `Ractor.select()` is `[r, msg]` where `r` is a terminated Ractor and `msg` is the value of Ractor's block. Wait for a single ractor (same as `Ractor#value`): @@ -218,23 +214,21 @@ Waiting for two ractors: r1 = Ractor.new{'r1'} r2 = Ractor.new{'r2'} rs = [r1, r2] -as = [] +values = [] -# Wait for r1 or r2's Ractor.yield +# Wait for r1 or r2's termination r, obj = Ractor.select(*rs) rs.delete(r) -as << obj +values << obj # Second try (rs only contain not-closed ractors) r, obj = Ractor.select(*rs) rs.delete(r) -as << obj -as.sort == ['r1', 'r2'] #=> true +values << obj +values.sort == ['r1', 'r2'] #=> true ``` -TODO: Current `Ractor.select()` has the same issue of `select(2)`, so this interface should be refined. - -TODO: `select` syntax of go-language uses round-robin technique to make fair scheduling. Now `Ractor.select()` doesn't use it. +NOTE: Using `Ractor.select()` on a very large number of ractors has the same issue as `select(2)` currently. ### Closing Ractor's ports @@ -252,7 +246,7 @@ end r.join # success (wait for the termination) r.value # success (will return 'finish') -# the first Ractor which success the `Ractor#value` can get the result +# The ractor's termination value has already been given to another ractor Ractor.new r do |r| r.value #=> Ractor::Error end @@ -264,7 +258,7 @@ Example (try to send to closed (terminated) Ractor): r = Ractor.new do end -r.join # wait terminate +r.join # wait for termination begin r.send(1) @@ -289,7 +283,7 @@ end obj.object_id == r.value #=> false ``` -Some objects are not supported to copy the value, and raise an exception. +Some objects do not support copying, and raise an exception. ```ruby obj = Thread.new{} @@ -299,15 +293,13 @@ begin end rescue TypeError => e e.message #=> # -else - 'ng' # unreachable here end ``` ### Send a message by moving `Ractor::Port#send(obj, move: true)` moves `obj` to the destination Ractor. -If the source Ractor touches the moved object (for example, call the method like `obj.foo()`), it will be an error. +If the source Ractor touches the moved object (for example, calls a method like `obj.foo()`), it will raise an error. ```ruby # move with Ractor#send @@ -316,23 +308,21 @@ r = Ractor.new do obj << ' world' end -str = 'hello' +str = 'hello'.dup r.send str, move: true +# str is now moved, and accessing str from this Ractor is prohibited modified = r.value #=> 'hello world' -# str is moved, and accessing str from this Ractor is prohibited begin # Error because it touches moved str. str << ' exception' # raise Ractor::MovedError rescue Ractor::MovedError modified #=> 'hello world' -else - raise 'unreachable' end ``` -Some objects are not supported to move, and an exception will be raised. +Some objects do not support moving, and an exception will be raised. ```ruby r = Ractor.new do @@ -342,34 +332,29 @@ end r.send(Thread.new{}, move: true) #=> allocator undefined for Thread (TypeError) ``` -To achieve the access prohibition for moved objects, _class replacement_ technique is used to implement it. +Once an object has been moved, the source object's class is changed to `Ractor::MovedObject`. ### Shareable objects -The following objects are shareable. - -* Immutable objects - * Small integers, some symbols, `true`, `false`, `nil` (a.k.a. `SPECIAL_CONST_P()` objects in internal) - * Frozen native objects - * Numeric objects: `Float`, `Complex`, `Rational`, big integers (`T_BIGNUM` in internal) - * All Symbols. - * Frozen `String` and `Regexp` objects (their instance variables should refer only shareable objects) -* Class, Module objects (`T_CLASS`, `T_MODULE` and `T_ICLASS` in internal) -* `Ractor` and other special objects which care about synchronization. +The following is an inexhaustive list of shareable objects: -Implementation: Now shareable objects (`RVALUE`) have `FL_SHAREABLE` flag. This flag can be added lazily. +* Small integers, big integers, `Float`, `Complex`, `Rational` +* All symbols, frozen Strings, `true`, `false`, `nil` +* `Regexp` objects, if they have no instance variables or their instance variables refer only to shareables +* Class and Module objects +* `Ractor` and other special objects which care about synchronization -To make shareable objects, `Ractor.make_shareable(obj)` method is provided. In this case, try to make shareable by freezing `obj` and recursively traversable objects. This method accepts `copy:` keyword (default value is false).`Ractor.make_shareable(obj, copy: true)` tries to make a deep copy of `obj` and make the copied object shareable. +To make objects shareable, `Ractor.make_shareable(obj)` is provided. It tries to make the object shareable by freezing `obj` and recursively traversing its references to freeze them all. This method accepts the `copy:` keyword (default value is false). `Ractor.make_shareable(obj, copy: true)` tries to make a deep copy of `obj` and make the copied object shareable. `Ractor.make_shareable(copy: false)` has no effect on an already shareable object. If the object cannot be made shareable, a `Ractor::Error` exception will be raised. ## Language changes to isolate unshareable objects between Ractors To isolate unshareable objects between Ractors, we introduced additional language semantics on multi-Ractor Ruby programs. -Note that without using Ractors, these additional semantics is not needed (100% compatible with Ruby 2). +Note that without using Ractors, these additional semantics are not needed (100% compatible with Ruby 2). ### Global variables -Only the main Ractor (a Ractor created at starting of interpreter) can access global variables. +Only the main Ractor can access global variables. ```ruby $gv = 1 @@ -388,7 +373,7 @@ Note that some special global variables, such as `$stdin`, `$stdout` and `$stder ### Instance variables of shareable objects -Instance variables of classes/modules can be get from non-main Ractors if the referring values are shareable objects. +Instance variables of classes/modules can be accessed from non-main Ractors only if their values are shareable objects. ```ruby class C @@ -428,8 +413,6 @@ Ractor.new do end.join ``` - - ```ruby shared = Ractor.new{} shared.instance_variable_set(:@iv, 'str') @@ -445,8 +428,6 @@ rescue Ractor::RemoteError => e end ``` -Note that instance variables for class/module objects are also prohibited on Ractors. - ### Class variables Only the main Ractor can access class variables. @@ -472,11 +453,11 @@ end ### Constants -Only the main Ractor can read constants which refer to the unshareable object. +Only the main Ractor can read constants which refer to an unshareable object. ```ruby class C - CONST = 'str' + CONST = 'str'.dup end r = Ractor.new do C::CONST @@ -488,13 +469,13 @@ rescue => e end ``` -Only the main Ractor can define constants which refer to the unshareable object. +Only the main Ractor can define constants which refer to an unshareable object. ```ruby class C end r = Ractor.new do - C::CONST = 'str' + C::CONST = 'str'.dup end begin r.join @@ -503,19 +484,19 @@ rescue => e end ``` -To make multi-ractor supported library, the constants should only refer shareable objects. +When creating/updating a library to support ractors, constants should only refer to shareable objects if they are to be used by non-main ractors. ```ruby TABLE = {a: 'ko1', b: 'ko2', c: 'ko3'} ``` -In this case, `TABLE` references an unshareable Hash object. So that other ractors can not refer `TABLE` constant. To make it shareable, we can use `Ractor.make_shareable()` like that. +In this case, `TABLE` refers to an unshareable Hash object. In order for other ractors to use `TABLE`, we need to make it shareable. We can use `Ractor.make_shareable()` like so: ```ruby TABLE = Ractor.make_shareable( {a: 'ko1', b: 'ko2', c: 'ko3'} ) ``` -To make it easy, Ruby 3.0 introduced new `shareable_constant_value` Directive. +To make it easy, Ruby 3.0 introduced a new `shareable_constant_value` file directive. ```ruby # shareable_constant_value: literal @@ -524,7 +505,7 @@ TABLE = {a: 'ko1', b: 'ko2', c: 'ko3'} #=> Same as: TABLE = Ractor.make_shareable( {a: 'ko1', b: 'ko2', c: 'ko3'} ) ``` -`shareable_constant_value` directive accepts the following modes (descriptions use the example: `CONST = expr`): +The `shareable_constant_value` directive accepts the following modes (descriptions use the example: `CONST = expr`): * none: Do nothing. Same as: `CONST = expr` * literal: @@ -533,15 +514,71 @@ TABLE = {a: 'ko1', b: 'ko2', c: 'ko3'} * experimental_everything: replaced to `CONST = Ractor.make_shareable(expr)`. * experimental_copy: replaced to `CONST = Ractor.make_shareable(expr, copy: true)`. -Except the `none` mode (default), it is guaranteed that the assigned constants refer to only shareable objects. +Except for the `none` mode (default), it is guaranteed that the constants in the file refer only to shareable objects. See [doc/syntax/comments.rdoc](syntax/comments.rdoc) for more details. -## Implementation note +### Shareable procs + +Procs and lambdas are unshareable objects, even when they are frozen. To create an unshareable Proc, you must use `Ractor.shareable_proc { expr }`. Much like during Ractor creation, the Proc's block is isolated +from its outer environment, so it cannot access locals from the outside scope. `self` is also changed within the Proc to be `nil` by default, although a `self:` keyword can be provided if you want to customize +the value to a different shareable object. + +```ruby +p = Ractor.shareable_proc { p self } +p.call #=> nil +``` + +```ruby +begin + a = 1 + pr = Ractor.shareable_proc { p a } + pr.call # never gets here +rescue Ractor::IsolationError +end +``` -* Each Ractor has its own thread, it means each Ractor has at least 1 native thread. -* Each Ractor has its own ID (`rb_ractor_t::pub::id`). - * On debug mode, all unshareable objects are labeled with current Ractor's id, and it is checked to detect unshareable object leak (access an object from different Ractor) in VM. +In order to dynamically define a method with `define_method` that can be used from different ractors, you must +define it with a shareable proc. Alternatively, you can use `class_eval` or `module_eval` with a String. Even though +the shareable proc's `self` is initially bound to `nil`, `define_method` will bind `self` to the correct value in the +method. + +```ruby +class A + define_method :testing, &Ractor.shareable_proc do + p self + end +end +Ractor.new do + a = A.new + a.testing #=> # +end.join +``` + +This isolation must be done to prevent the method from accessing captured outer variables across Ractors. + +### Ractor-local storage + +You can store any object (even unshareables) in ractor-local storage. + +```ruby +r = Ractor.new do + values = [] + Ractor[:threads] = [] + 3.times do |i| + Ractor[:threads] << Thread.new do + values << [Ractor.receive, i+1] # Ractor.receive blocks the current thread in the current ractor until it receives a message + end + end + Ractor[:threads].each(&:join) + values +end + +r << 1 +r << 2 +r << 3 +r.value #=> [[1,1],[2,2],[3,3]] (the order can change with each run) +``` ## Examples From 48842525a8c3270b5dcc2b0a47f010658ef641a2 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 20 Dec 2025 17:05:11 -0500 Subject: [PATCH 2/7] Exclude TestObjSpace#test_dump_objects_dumps_page_slot_sizes for MMTk [ci skip] --- test/.excludes-mmtk/TestObjSpace.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/.excludes-mmtk/TestObjSpace.rb b/test/.excludes-mmtk/TestObjSpace.rb index 05666e46f07fc8..94eb2c436d4435 100644 --- a/test/.excludes-mmtk/TestObjSpace.rb +++ b/test/.excludes-mmtk/TestObjSpace.rb @@ -1,3 +1,4 @@ exclude(:test_dump_all_full, "testing behaviour specific to default GC") exclude(:test_dump_flag_age, "testing behaviour specific to default GC") exclude(:test_dump_flags, "testing behaviour specific to default GC") +exclude(:test_dump_objects_dumps_page_slot_sizes, "testing behaviour specific to default GC") From 7c1e37cfe13cf2df21e4ca23c91f5a53c81bc062 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Sat, 20 Dec 2025 22:57:00 +0000 Subject: [PATCH 3/7] [DOC] Tweaks for Module#<=> --- object.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/object.c b/object.c index b2b7a9d96038f0..93c8a483cfe26c 100644 --- a/object.c +++ b/object.c @@ -2012,13 +2012,15 @@ rb_mod_gt(VALUE mod, VALUE arg) /* * call-seq: - * self <=> object -> -1, 0, +1, or nil + * self <=> other -> -1, 0, 1, or nil + * + * Compares +self+ and +other+. * * Returns: * - * - +-1+, if +self+ includes +object+, if or +self+ is a subclass of +object+. - * - +0+, if +self+ and +object+ are the same. - * - +1+, if +object+ includes +self+, or if +object+ is a subclass of +self+. + * - +-1+, if +self+ includes +other+, if or +self+ is a subclass of +other+. + * - +0+, if +self+ and +other+ are the same. + * - +1+, if +other+ includes +self+, or if +other+ is a subclass of +self+. * - +nil+, if none of the above is true. * * Examples: @@ -2029,8 +2031,10 @@ rb_mod_gt(VALUE mod, VALUE arg) * Enumerable <=> Array # => 1 * # Class File is a subclass of class IO. * File <=> IO # => -1 - * IO <=> File # => 1 * File <=> File # => 0 + * IO <=> File # => 1 + * # Class File has no relationship to class String. + * File <=> String # => nil * */ From addbeb6cf337695647db6e265b5db87105d1c2e9 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Sat, 20 Dec 2025 22:22:35 +0000 Subject: [PATCH 4/7] [DOC] Note for String#<=> about Comparable --- string.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/string.c b/string.c index 83219d1a26d085..8c7c82c10f4fa8 100644 --- a/string.c +++ b/string.c @@ -4315,6 +4315,9 @@ rb_str_eql(VALUE str1, VALUE str2) * 'ab' <=> 'a' # => 1 * 'a' <=> :a # => nil * + * \Class \String includes module Comparable, + * each of whose methods uses String#<=> for comparison. + * * Related: see {Comparing}[rdoc-ref:String@Comparing]. */ From 580872785181303f26f715b5acc611d0ec087256 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Sat, 20 Dec 2025 22:30:03 +0000 Subject: [PATCH 5/7] [DOC] Fix indentation error in Complex#<=> --- complex.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/complex.c b/complex.c index 1fe68f80bebb97..bb54d4f61f31ca 100644 --- a/complex.c +++ b/complex.c @@ -1282,8 +1282,8 @@ nucomp_real_p(VALUE self) * Complex.rect(1) <=> Complex.rect(1, 1) # => nil # object.imag not zero. * Complex.rect(1) <=> 'Foo' # => nil # object.imag not defined. * - * \Class \Complex includes module Comparable, - * each of whose methods uses Complex#<=> for comparison. + * \Class \Complex includes module Comparable, + * each of whose methods uses Complex#<=> for comparison. */ static VALUE nucomp_cmp(VALUE self, VALUE other) From f42535fa38ff487382db32158908635e1a356d5c Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 20 Dec 2025 16:33:21 -0500 Subject: [PATCH 6/7] Change test to define ivars in initialize method Defining ivars in initialize method guarantees the object to be embedded. --- test/ruby/test_object.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/ruby/test_object.rb b/test/ruby/test_object.rb index cccd7359e1108e..41585ba150e160 100644 --- a/test/ruby/test_object.rb +++ b/test/ruby/test_object.rb @@ -359,11 +359,13 @@ def test_remove_instance_variable_re_embed require "objspace" c = Class.new do - def a = @a + attr_reader :a, :b, :c - def b = @b - - def c = @c + def initialize + @a = nil + @b = nil + @c = nil + end end o1 = c.new From 483ef3a0d2a6c097ca1606c5cb4a0fae8f3d4f43 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 20 Dec 2025 17:01:05 -0500 Subject: [PATCH 7/7] Test test_remove_instance_variable_re_embed separately Shape tree pollution could cause this test to flake. --- test/ruby/test_object.rb | 63 ++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/test/ruby/test_object.rb b/test/ruby/test_object.rb index 41585ba150e160..f4dfe2251b884f 100644 --- a/test/ruby/test_object.rb +++ b/test/ruby/test_object.rb @@ -356,40 +356,41 @@ def test_remove_instance_variable end def test_remove_instance_variable_re_embed - require "objspace" + assert_separately(%w[-robjspace], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + c = Class.new do + attr_reader :a, :b, :c - c = Class.new do - attr_reader :a, :b, :c - - def initialize - @a = nil - @b = nil - @c = nil + def initialize + @a = nil + @b = nil + @c = nil + end end - end - o1 = c.new - o2 = c.new - - o1.instance_variable_set(:@foo, 5) - o1.instance_variable_set(:@a, 0) - o1.instance_variable_set(:@b, 1) - o1.instance_variable_set(:@c, 2) - refute_includes ObjectSpace.dump(o1), '"embedded":true' - o1.remove_instance_variable(:@foo) - assert_includes ObjectSpace.dump(o1), '"embedded":true' - - o2.instance_variable_set(:@a, 0) - o2.instance_variable_set(:@b, 1) - o2.instance_variable_set(:@c, 2) - assert_includes ObjectSpace.dump(o2), '"embedded":true' - - assert_equal(0, o1.a) - assert_equal(1, o1.b) - assert_equal(2, o1.c) - assert_equal(0, o2.a) - assert_equal(1, o2.b) - assert_equal(2, o2.c) + o1 = c.new + o2 = c.new + + o1.instance_variable_set(:@foo, 5) + o1.instance_variable_set(:@a, 0) + o1.instance_variable_set(:@b, 1) + o1.instance_variable_set(:@c, 2) + refute_includes ObjectSpace.dump(o1), '"embedded":true' + o1.remove_instance_variable(:@foo) + assert_includes ObjectSpace.dump(o1), '"embedded":true' + + o2.instance_variable_set(:@a, 0) + o2.instance_variable_set(:@b, 1) + o2.instance_variable_set(:@c, 2) + assert_includes ObjectSpace.dump(o2), '"embedded":true' + + assert_equal(0, o1.a) + assert_equal(1, o1.b) + assert_equal(2, o1.c) + assert_equal(0, o2.a) + assert_equal(1, o2.b) + assert_equal(2, o2.c) + end; end def test_convert_string