Bug #20012
closedFix keyword splat passing as regular argument
Description
Since Ruby 3.0, Ruby has passed a keyword splat as a regular argument in the case of a call to a Ruby method where the method does not accept keyword arguments, if the method call does not contain an argument splat:
def self.f(obj) obj end
def self.fs(*obj) obj[0] end
h = {a: 1}
f(**h).equal?(h) # Before: true; After: false
fs(**h).equal?(h) # Before: true; After: false
a = []
f(*a, **h).equal?(h) # Before and After: false
fs(*a, **h).equal?(h) # Before and After: false
The fact that the behavior differs when passing an empty argument splat makes it obvious that something is not working the way it is intended. The fact that the hash is copied for C methods also makes it obvious. Ruby 2 always copied the keyword splat hash, and I think that is the expected behavior in Ruby 3.
This bug is because of a missed check in setup_parameters_complex. If the keyword splat passed is not mutable, then it points to an existing object and not a new object, and therefore it must be copied.
I did not bisect to find the commit that introduced the problem, but this seems likely to be something I introduced during keyword argument separation. I'm surprised that this has not been filed as a bug sooner. Maybe it just rarely comes up in practice, and I only discovered it recently. Possibly, other developers that may have discovered this may have decided against submitting this as an issue, as we have had specs for two years that give the impression that the broken behavior was expected.
I've submitted a pull request to fix this: https://github.com/ruby/ruby/pull/8970