Project

General

Profile

Actions

Bug #21525

closed

Inconsistent execution order for methods with constant blocks

Added by joel@drapper.me (Joel Drapper) 2 days ago. Updated 1 day ago.

Status:
Closed
Assignee:
-
Target version:
-
ruby -v:
ruby 3.4.5 (2025-08-01 revision 07f7832cff) +PRISM [arm64-darwin24]
[ruby-core:122892]

Description

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.

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.

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.

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].


Related issues 1 (0 open1 closed)

Related to Ruby - Bug #20640: Evaluation Order Issue in f(**h, &h.delete(key))Closedjeremyevans0 (Jeremy Evans)Actions

Updated by jeremyevans0 (Jeremy Evans) 2 days ago

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:

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?

Actions #2

Updated by mame (Yusuke Endoh) 2 days ago

  • Related to Bug #20640: Evaluation Order Issue in f(**h, &h.delete(key)) added

Updated by joel@drapper.me (Joel Drapper) 2 days ago · Edited

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 these extreme edge cases.

There are much worse examples of where Ruby executes out of order, such as

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.

Actions #4

Updated by jeremyevans0 (Jeremy Evans) 1 day ago

  • Status changed from Open to Closed
Actions

Also available in: Atom PDF

Like0
Like1Like0Like0Like0