Bug #22525
openRactor.shareable_proc sees nil for a block parameter of the method which was not read yet
Description
When a shareable Proc reads a block parameter (&b) of the enclosing method, it gets nil if the method did not read b before. It does not raise Ractor::IsolationError even when the block is not shareable, and it loses the block even when the block is shareable:
def read_block(&b) = Ractor.shareable_proc { b }
p read_block { 1 }.call # expected: Ractor::IsolationError, as the block is not shareable
shareable = Ractor.shareable_proc { 2 }
p read_block(&shareable).call # expected: the shareable Proc
$ ruby -v blockparam.rb
ruby 4.1.0dev (2026-11-09T15:47:20Z master 7e5d882817) +PRISM [arm64-darwin25]
nil
nil
Same result on 4.0.7. Since #22444 this also applies to Ractor.new blocks.
Cause¶
The block parameter of a method is only set when it is first read: getblockparam (and getblockparamproxy for b.call) creates the Proc from the block handler of the method's frame and stores it in the local variable slot. Until then, the slot holds nil.
When making a Proc shareable, env_copy copies the slot, which is still nil. The copied environment has no block handler, so reading b in the shareable Proc gives nil.
Binding#local_variable_get already handles this case (get_local_variable_ptr in proc.c) by setting the block parameter first.
Additionally, b.call, b[], b.yield and b === x are compiled to getblockparamproxy without recording b as an outer variable of the block, so for these b is not checked nor copied at all:
def call_block(&b) = Ractor.shareable_proc { b.call }
call_block { 1 } # no error, the shareable Proc raises NoMethodError when called
Fix¶
PR: https://github.com/ruby/ruby/pull/19808
The fix does the same in env_copy: it sets the block parameter before checking whether its value is shareable and copying it. It also marks the block parameter as set in the copied environment, so that the shareable Proc reads the copied value. It also records b as an outer variable for b.call and the like, in both prism_compile.c and compile.c.
This also matters for https://github.com/ruby/ruby/pull/19801 (making the body of define_method shareable when possible). With that change, this pattern from rspec-support (and similar code in ActiveRecord) got nil instead of the block:
def self.define_optimized_require_for_rspec(lib, &require_relative)
define_singleton_method("require_rspec_#{lib}") do |f|
require_relative.call("#{lib}/#{f}")
end
end
With the fix, such a body is not made shareable, because the block is not shareable, and the method works as before.
No data to display