[ruby-core:122892] [Ruby Bug#21525] Inconsistent execution order for methods with constant blocks

Issue #21525 has been reported by joel@drapper.me (Joel Drapper). ---------------------------------------- Bug #21525: Inconsistent execution order for methods with constant blocks https://bugs.ruby-lang.org/issues/21525 * Author: joel@drapper.me (Joel Drapper) * Status: Open * ruby -v: ruby 3.4.5 (2025-08-01 revision 07f7832cff) +PRISM [arm64-darwin24] * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN ---------------------------------------- If you call a method and pass in a splat of an array with a block argument coerced from calling a method, Ruby maintains the expected execution order and the called method receives a copy of the array prior to modification. In this example, the output is `[1, 2, 3]` since the copy is made before the `bar` method can modify it. ```ruby ARRAY = [1, 2, 3] def bar ARRAY.pop -> {} end def example(*args, &) puts args end example(*ARRAY, &bar) ``` However, when passing a constant, the block coercion itself is able to modify the array. ```ruby ARRAY = [1, 2, 3] module Foo module Bar def self.to_proc ARRAY.pop -> {} end end end def example(*args, &) puts args end example(*ARRAY, &Foo::Bar) ``` Another way to trigger this is to define a `const_missing` method that modifies the array. ```ruby ARRAY = [1, 2, 3] module Foo def self.const_missing(name) ARRAY.pop -> {} end end def example(*args, &) puts args end example(*ARRAY, &Foo::Bar) ``` In both these cases, the output is `[1, 2]` instead of the expected `[1, 2, 3]`. -- https://bugs.ruby-lang.org/

Issue #21525 has been updated by jeremyevans0 (Jeremy Evans). Avoiding allocation is deliberate trade-off for these cases, and not the only case where Ruby does this. There are other corner cases where Ruby skips allocating in cases where pathological code could result in an evaluation order issue. For example: ```ruby method(*args, **kwargs) method(*args, &block) method(**kwargs, &block) ``` All have similar evaluation order issues if `kwargs` isn't a hash or `block` isn't a proc, and `kwargs.to_hash` or `block.to_proc` modify `args` or `kwargs`. Ruby deliberate skips allocating in these cases, because otherwise, all of these cases would require VM instructions that allocate, and it doesn't seem worth the performance hit just to handle pathlogical cases correctly. Are you aware of any cases where production code could be affected by avoiding allocation for these case? Can you think of a case that is reasonable and not pathological? Do you think the benefits of require allocation in these cases outweigh the performance costs? ---------------------------------------- Bug #21525: Inconsistent execution order for methods with constant blocks https://bugs.ruby-lang.org/issues/21525#change-114194 * Author: joel@drapper.me (Joel Drapper) * Status: Open * ruby -v: ruby 3.4.5 (2025-08-01 revision 07f7832cff) +PRISM [arm64-darwin24] * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN ---------------------------------------- If you call a method and pass in a splat of an array with a block argument coerced from calling a method, Ruby maintains the expected execution order and the called method receives a copy of the array prior to modification. In this example, the output is `[1, 2, 3]` since the copy is made before the `bar` method can modify it. ```ruby ARRAY = [1, 2, 3] def bar ARRAY.pop -> {} end def example(*args, &) puts args end example(*ARRAY, &bar) ``` However, when passing a constant, the block coercion itself is able to modify the array. ```ruby ARRAY = [1, 2, 3] module Foo module Bar def self.to_proc ARRAY.pop -> {} end end end def example(*args, &) puts args end example(*ARRAY, &Foo::Bar) ``` Another way to trigger this is to define a `const_missing` method that modifies the array. ```ruby ARRAY = [1, 2, 3] module Foo def self.const_missing(name) ARRAY.pop -> {} end end def example(*args, &) puts args end example(*ARRAY, &Foo::Bar) ``` In both these cases, the output is `[1, 2]` instead of the expected `[1, 2, 3]`. -- https://bugs.ruby-lang.org/

Issue #21525 has been updated by joel@drapper.me (Joel Drapper). The behaviour is a little surprising if you don’t understand why it’s different in these cases. But no I can’t think of any non-pathological reason to actually do this in production code and I agree that the performance benefit is probably worth the cost of slight inconsistency in extreme edge cases. There are much worse examples of where Ruby executes out of order, such as ```ruby b.push(bar) while (bar = a.pop) ``` This one actually has caught me out with production code a number of times and still catches me out regularly. ---------------------------------------- Bug #21525: Inconsistent execution order for methods with constant blocks https://bugs.ruby-lang.org/issues/21525#change-114198 * Author: joel@drapper.me (Joel Drapper) * Status: Open * ruby -v: ruby 3.4.5 (2025-08-01 revision 07f7832cff) +PRISM [arm64-darwin24] * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN ---------------------------------------- If you call a method and pass in a splat of an array with a block argument coerced from calling a method, Ruby maintains the expected execution order and the called method receives a copy of the array prior to modification. In this example, the output is `[1, 2, 3]` since the copy is made before the `bar` method can modify it. ```ruby ARRAY = [1, 2, 3] def bar ARRAY.pop -> {} end def example(*args, &) puts args end example(*ARRAY, &bar) ``` However, when passing a constant, the block coercion itself is able to modify the array. ```ruby ARRAY = [1, 2, 3] module Foo module Bar def self.to_proc ARRAY.pop -> {} end end end def example(*args, &) puts args end example(*ARRAY, &Foo::Bar) ``` Another way to trigger this is to define a `const_missing` method that modifies the array. ```ruby ARRAY = [1, 2, 3] module Foo def self.const_missing(name) ARRAY.pop -> {} end end def example(*args, &) puts args end example(*ARRAY, &Foo::Bar) ``` In both these cases, the output is `[1, 2]` instead of the expected `[1, 2, 3]`. -- https://bugs.ruby-lang.org/
participants (2)
-
jeremyevans0 (Jeremy Evans)
-
joel@drapper.me (Joel Drapper)