Bug #21525
closedInconsistent execution order for methods with constant blocks
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]
.
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?
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.
Updated by jeremyevans0 (Jeremy Evans) 1 day ago
- Status changed from Open to Closed