From 2b2a1f0c2afd0fe6b66eb3bea9d18e65240f0a75 Mon Sep 17 00:00:00 2001 From: Herwin Date: Wed, 4 Feb 2026 15:56:05 +0100 Subject: [PATCH 1/2] Import specs for Fiber#transfer --- spec/core/fiber/transfer_spec.rb | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 spec/core/fiber/transfer_spec.rb diff --git a/spec/core/fiber/transfer_spec.rb b/spec/core/fiber/transfer_spec.rb new file mode 100644 index 0000000000..238721475d --- /dev/null +++ b/spec/core/fiber/transfer_spec.rb @@ -0,0 +1,84 @@ +require_relative '../../spec_helper' +require_relative 'shared/resume' + +describe "Fiber#transfer" do + it_behaves_like :fiber_resume, :transfer +end + +describe "Fiber#transfer" do + it "transfers control from one Fiber to another when called from a Fiber" do + fiber1 = Fiber.new { :fiber1 } + fiber2 = Fiber.new { fiber1.transfer; :fiber2 } + fiber2.resume.should == :fiber2 + end + + it "returns to the root Fiber when finished" do + f1 = Fiber.new { :fiber_1 } + f2 = Fiber.new { f1.transfer; :fiber_2 } + + f2.transfer.should == :fiber_1 + f2.transfer.should == :fiber_2 + end + + it "can be invoked from the same Fiber it transfers control to" do + states = [] + fiber = Fiber.new { states << :start; fiber.transfer; states << :end } + fiber.transfer + states.should == [:start, :end] + + states = [] + fiber = Fiber.new { states << :start; fiber.transfer; states << :end } + fiber.resume + states.should == [:start, :end] + end + + it "can not transfer control to a Fiber that has suspended by Fiber.yield" do + states = [] + fiber1 = Fiber.new { states << :fiber1 } + fiber2 = Fiber.new { states << :fiber2_start; Fiber.yield fiber1.transfer; states << :fiber2_end} + fiber2.resume.should == [:fiber2_start, :fiber1] + -> { fiber2.transfer }.should raise_error(FiberError) + end + + it "raises a FiberError when transferring to a Fiber which resumes itself" do + fiber = Fiber.new { fiber.resume } + -> { fiber.transfer }.should raise_error(FiberError) + end + + it "works if Fibers in different Threads each transfer to a Fiber in the same Thread" do + # This catches a bug where Fibers are running on a thread-pool + # and Fibers from a different Ruby Thread reuse the same native thread. + # Caching the Ruby Thread based on the native thread is not correct in that case, + # and the check for "fiber called across threads" in Fiber#transfer + # might be incorrect based on that. + 2.times do + Thread.new do + io_fiber = Fiber.new do |calling_fiber| + calling_fiber.transfer + end + io_fiber.transfer(Fiber.current) + value = Object.new + io_fiber.transfer(value).should equal value + end.join + end + end + + it "transfers control between a non-main thread's root fiber to a child fiber and back again" do + states = [] + thread = Thread.new do + f1 = Fiber.new do |f0| + states << 0 + value2 = f0.transfer(1) + states << value2 + 3 + end + + value1 = f1.transfer(Fiber.current) + states << value1 + value3 = f1.transfer(2) + states << value3 + end + thread.join + states.should == [0, 1, 2, 3] + end +end From 1545b026178f31129799de8563c36d99bae81dc9 Mon Sep 17 00:00:00 2001 From: Herwin Date: Wed, 4 Feb 2026 15:59:10 +0100 Subject: [PATCH 2/2] Add primitive Fiber#transfer It's just an alternative for Fiber#resume, this fails a few specs but works mostly. This is a nice way to kick things off. --- include/natalie/fiber_object.hpp | 1 + lib/natalie/compiler/binding_gen.rb | 1 + spec/core/fiber/current_spec.rb | 4 +- spec/core/fiber/inspect_spec.rb | 6 +-- spec/core/fiber/resume_spec.rb | 8 ++-- spec/core/fiber/transfer_spec.rb | 65 +++++++++++++++++------------ src/fiber_object.cpp | 5 +++ 7 files changed, 53 insertions(+), 37 deletions(-) diff --git a/include/natalie/fiber_object.hpp b/include/natalie/fiber_object.hpp index 783c75ee87..9bd0b53949 100644 --- a/include/natalie/fiber_object.hpp +++ b/include/natalie/fiber_object.hpp @@ -77,6 +77,7 @@ class FiberObject : public Object { static Value set_scheduler(Env *, Value); Value set_storage(Env *, Value); Value storage(Env *) const; + Value transfer(Env *env, Args &&args); void swap_to_previous(Env *env, size_t arg_size, Value *arg_data); mco_coro *coroutine() { return m_coroutine; } diff --git a/lib/natalie/compiler/binding_gen.rb b/lib/natalie/compiler/binding_gen.rb index 93645adf70..a19209e539 100644 --- a/lib/natalie/compiler/binding_gen.rb +++ b/lib/natalie/compiler/binding_gen.rb @@ -605,6 +605,7 @@ def generate_name gen.binding('Fiber', 'storage', 'FiberObject', 'storage', argc: 0, pass_env: true, pass_block: false, return_type: :Value) gen.binding('Fiber', 'storage=', 'FiberObject', 'set_storage', argc: 1, pass_env: true, pass_block: false, return_type: :Value) gen.binding('Fiber', 'to_s', 'FiberObject', 'inspect', argc: 0, pass_env: true, pass_block: false, aliases: ['inspect'], return_type: :Value) +gen.binding('Fiber', 'transfer', 'FiberObject', 'transfer', argc: :any, pass_env: true, pass_block: false, return_type: :Value) gen.static_binding_as_class_method('File', 'absolute_path', 'FileObject', 'absolute_path', argc: 1..2, pass_env: true, pass_block: false, return_type: :Value) gen.static_binding_as_class_method('File', 'absolute_path?', 'FileObject', 'is_absolute_path', argc: 1, pass_env: true, pass_block: false, return_type: :bool) diff --git a/spec/core/fiber/current_spec.rb b/spec/core/fiber/current_spec.rb index 9145a355c4..48e8e18f09 100644 --- a/spec/core/fiber/current_spec.rb +++ b/spec/core/fiber/current_spec.rb @@ -6,7 +6,7 @@ root.should be_an_instance_of(Fiber) # We can always transfer to the root Fiber; it will never die 5.times do - NATFIXME 'Implement Fiber#transfer', exception: NoMethodError, message: "undefined method 'transfer'" do + NATFIXME 'Implement Fiber#transfer', exception: FiberError, message: 'attempt to resume a resuming fiber' do root.transfer.should be_nil end root.alive?.should be_true @@ -24,7 +24,7 @@ end it "returns the current Fiber when called from a Fiber that transferred to another" do - NATFIXME 'Implement Fiber#transfer', exception: NoMethodError, message: "undefined method 'transfer'" do + NATFIXME 'Implement Fiber#transfer', exception: SpecFailedException do states = [] fiber = Fiber.new do states << :fiber diff --git a/spec/core/fiber/inspect_spec.rb b/spec/core/fiber/inspect_spec.rb index 9832069422..1732e7ca80 100644 --- a/spec/core/fiber/inspect_spec.rb +++ b/spec/core/fiber/inspect_spec.rb @@ -20,10 +20,8 @@ end it "is resumed for a Fiber which was transferred" do - NATFIXME 'Implement Fiber#transfer', exception: NoMethodError, message: "undefined method 'transfer'" do - inspected = Fiber.new { Fiber.current.inspect }.transfer - inspected.should =~ /\A#\z/ - end + inspected = Fiber.new { Fiber.current.inspect }.transfer + inspected.should =~ /\A#\z/ end it "is suspended for a Fiber which was resumed and yielded" do diff --git a/spec/core/fiber/resume_spec.rb b/spec/core/fiber/resume_spec.rb index e88cff9d61..4b20f4b4bf 100644 --- a/spec/core/fiber/resume_spec.rb +++ b/spec/core/fiber/resume_spec.rb @@ -70,11 +70,9 @@ it "can work with Fiber#transfer" do fiber1 = Fiber.new { true } - NATFIXME 'Implement Fiber#transfer', exception: NoMethodError, message: "undefined method 'transfer' for an instance of Fiber" do - fiber2 = Fiber.new { fiber1.transfer; Fiber.yield 10 ; Fiber.yield 20; raise } - fiber2.resume.should == 10 - fiber2.resume.should == 20 - end + fiber2 = Fiber.new { fiber1.transfer; Fiber.yield 10 ; Fiber.yield 20; raise } + fiber2.resume.should == 10 + fiber2.resume.should == 20 end it "raises a FiberError if the Fiber attempts to resume a resuming fiber" do diff --git a/spec/core/fiber/transfer_spec.rb b/spec/core/fiber/transfer_spec.rb index 238721475d..c593e3f17d 100644 --- a/spec/core/fiber/transfer_spec.rb +++ b/spec/core/fiber/transfer_spec.rb @@ -16,20 +16,24 @@ f1 = Fiber.new { :fiber_1 } f2 = Fiber.new { f1.transfer; :fiber_2 } - f2.transfer.should == :fiber_1 - f2.transfer.should == :fiber_2 + NATFIXME 'it returns to the root Fiber when finished', exception: SpecFailedException do + f2.transfer.should == :fiber_1 + f2.transfer.should == :fiber_2 + end end it "can be invoked from the same Fiber it transfers control to" do states = [] - fiber = Fiber.new { states << :start; fiber.transfer; states << :end } - fiber.transfer - states.should == [:start, :end] + NATFIXME 'it can be invoked from the same Fiber it transfers control to', exception: FiberError, message: 'attempt to resume the current fiber' do + fiber = Fiber.new { states << :start; fiber.transfer; states << :end } + fiber.transfer + states.should == [:start, :end] - states = [] - fiber = Fiber.new { states << :start; fiber.transfer; states << :end } - fiber.resume - states.should == [:start, :end] + states = [] + fiber = Fiber.new { states << :start; fiber.transfer; states << :end } + fiber.resume + states.should == [:start, :end] + end end it "can not transfer control to a Fiber that has suspended by Fiber.yield" do @@ -37,7 +41,9 @@ fiber1 = Fiber.new { states << :fiber1 } fiber2 = Fiber.new { states << :fiber2_start; Fiber.yield fiber1.transfer; states << :fiber2_end} fiber2.resume.should == [:fiber2_start, :fiber1] - -> { fiber2.transfer }.should raise_error(FiberError) + NATFIXME 'it can not transfer control to a Fiber that has suspended by Fiber.yield', exception: SpecFailedException, message: /but instead raised nothing/ do + -> { fiber2.transfer }.should raise_error(FiberError) + end end it "raises a FiberError when transferring to a Fiber which resumes itself" do @@ -53,12 +59,15 @@ # might be incorrect based on that. 2.times do Thread.new do - io_fiber = Fiber.new do |calling_fiber| - calling_fiber.transfer + NATFIXME 'it works if Fibers in different Threads each transfer to a Fiber in the same Thread', exception: FiberError, message: 'attempt to resume a resuming fiber' do + # This catches a bug where Fibers are running on a thread-pool + io_fiber = Fiber.new do |calling_fiber| + calling_fiber.transfer + end + io_fiber.transfer(Fiber.current) + value = Object.new + io_fiber.transfer(value).should equal value end - io_fiber.transfer(Fiber.current) - value = Object.new - io_fiber.transfer(value).should equal value end.join end end @@ -66,19 +75,23 @@ it "transfers control between a non-main thread's root fiber to a child fiber and back again" do states = [] thread = Thread.new do - f1 = Fiber.new do |f0| - states << 0 - value2 = f0.transfer(1) - states << value2 - 3 - end + NATFIXME "it transfers control between a non-main thread's root fiber to a child fiber and back again", exception: FiberError, message: 'attempt to resume a resuming fiber' do + f1 = Fiber.new do |f0| + states << 0 + value2 = f0.transfer(1) + states << value2 + 3 + end - value1 = f1.transfer(Fiber.current) - states << value1 - value3 = f1.transfer(2) - states << value3 + value1 = f1.transfer(Fiber.current) + states << value1 + value3 = f1.transfer(2) + states << value3 + end end thread.join - states.should == [0, 1, 2, 3] + NATFIXME "it transfers control between a non-main thread's root fiber to a child fiber and back again", exception: SpecFailedException do + states.should == [0, 1, 2, 3] + end end end diff --git a/src/fiber_object.cpp b/src/fiber_object.cpp index 9e643d6733..566950d7f8 100644 --- a/src/fiber_object.cpp +++ b/src/fiber_object.cpp @@ -249,6 +249,11 @@ Value FiberObject::storage(Env *env) const { return fiber->m_storage; } +// NATFIXME: For now, it is just the same as Fiber#resume +Value FiberObject::transfer(Env *env, Args &&args) { + return resume(env, std::move(args)); +} + NO_SANITIZE_ADDRESS Value FiberObject::yield(Env *env, Args args) { auto current_fiber = FiberObject::current(); auto previous_fiber = current_fiber->m_previous_fiber;