Project

General

Profile

Bug #16519

pp [Hash.ruby2_keywords_hash({})] shows `[nil]`

Added by Eregon (Benoit Daloze) 6 months ago. Updated 4 months ago.

Status:
Closed
Priority:
Normal
Target version:
-
ruby -v:
ruby 2.8.0dev (2020-01-21T13:45:10Z master 5798d35ff6) [x86_64-linux]
[ruby-core:96957]

Description

This happens on master:

$ ruby -ve 'ruby2_keywords def flag(*a); a.last; end; pp [flag(**{})]'
ruby 2.8.0dev (2020-01-21T13:45:10Z master 5798d35ff6) [x86_64-linux]
[nil]

Of course it should be [{}], as it is for pp [{}].

On 2.7.0 it warns (should be fixed, it's valid to pp a flagged Hash):

$ ruby -ve 'ruby2_keywords def flag(*a); a.last; end; pp [flag(**{})]'
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]
[/home/eregon/.rubies/ruby-2.7.0/lib/ruby/2.7.0/pp.rb:226: warning: Passing the keyword argument as the last hash parameter is deprecated
/home/eregon/.rubies/ruby-2.7.0/lib/ruby/2.7.0/pp.rb:334: warning: The called method is defined here
{}]

The warning being in the middle of the output is a fun fact here.
Lines it refers to (still the same on current master):
https://github.com/ruby/ruby/blob/v2_7_0/lib/pp.rb#L226
https://github.com/ruby/ruby/blob/v2_7_0/lib/pp.rb#L334

This is very confusing as it can happen during test-all and then show output such as:

<[{:a=>1}]> expected but was
<[{:a=>1}, nil]>.

when the reality is (can be verified with p before the assert_equal):

<[{:a=>1}]> expected but was
<[{:a=>1}, {}]>.

Updated by Eregon (Benoit Daloze) 6 months ago

The code around there looks like:

def seplist(list, sep=nil, iter_method=:each) # :yield: element
  sep ||= lambda { comma_breakable }
  first = true
  list.__send__(iter_method) {|*v|
    if first
      first = false
    else
      sep.call
    end
    yield(*v) # warning: Passing the keyword argument as the last hash parameter is deprecated
  }
end

class Array # :nodoc:
  def pretty_print(q) # :nodoc:
  q.group(1, '[', ']') {
    q.seplist(self) {|v| # warning: The called method is defined here
      q.pp v
    }
  }
  end
end

I don't know why it goes wrong.

#2

Updated by Eregon (Benoit Daloze) 6 months ago

  • Description updated (diff)

Updated by jeremyevans0 (Jeremy Evans) 6 months ago

Eregon (Benoit Daloze) wrote:

The code around there looks like:

def seplist(list, sep=nil, iter_method=:each) # :yield: element
  sep ||= lambda { comma_breakable }
  first = true
  list.__send__(iter_method) {|*v|
    if first
      first = false
    else
      sep.call
    end
    yield(*v) # warning: Passing the keyword argument as the last hash parameter is deprecated
  }
end

class Array # :nodoc:
  def pretty_print(q) # :nodoc:
  q.group(1, '[', ']') {
    q.seplist(self) {|v| # warning: The called method is defined here
      q.pp v
    }
  }
  end
end

I don't know why it goes wrong.

It goes wrong because a ruby2_keywords flagged hash is passed where a regular positional argument is expected. This is the the condition I warned about in #16463, the reason I am against ruby2_keywords by default. As I stated there, "I think such magical behavior is likely to introduce difficult to diagnose breakage in some cases, with no obvious reason why the code is not working correctly." To paraphrase Eregon (Benoit Daloze) , if a ruby committer heavily involved in the process cannot figure out even why a problem occurs, let alone the solution, what chance does a standard ruby programmer have?

I think matz (Yukihiro Matsumoto) should strongly consider this particular problem when deciding whether to accept ruby2_keywords by default. In Eregon (Benoit Daloze) 's dev meeting presentation, he said of this type of code: "In some rare cases, users might want to treat keyword arguments as positional". Perhaps this type of code is not as rare as once believed.

Fix for this case is in https://github.com/ruby/ruby/pull/2855

Updated by Eregon (Benoit Daloze) 6 months ago

jeremyevans0 (Jeremy Evans) wrote:

I think matz (Yukihiro Matsumoto) should strongly consider this particular problem when deciding whether to accept ruby2_keywords by default. In Eregon (Benoit Daloze) 's dev meeting presentation, he said of this type of code: "In some rare cases, users might want to treat keyword arguments as positional". Perhaps this type of code is not as rare as once believed.

I believe that is an incorrect conclusion.
This bug happens because ruby2_keywords exists (maybe we can fix **empty_hash or **empty_flagged_hash though, see below).
I was originally against ruby2_keywords because it's not purely lexical and therefore more complicated, but this is a long lost battle now.
Having ruby2_keywords by default changes exactly nothing about this specific bug.
It happens just the same with or without https://github.com/ruby/ruby/pull/2853.

Updated by Eregon (Benoit Daloze) 6 months ago

To paraphrase Eregon (Benoit Daloze) , if a ruby committer heavily involved in the process cannot figure out even why a problem occurs, let alone the solution, what chance does a standard ruby programmer have?

For the record, by "I don't know why it goes wrong." I meant "This is after midnight and I have no time to investigate it further today".
But yes, I'm surprised a ruby2_keywords flagged Hash is just removed and "lost".
I think we might want to fix that.

I think the actual issue here might be that we remove empty keyword arguments, which is also inconsistent with non-empty ones when they are given to a method not accepting keyword arguments:

def target(*args)
  args
end

# target does not accept keyword arguments, it shouldn't matter if we pass a last positional Hash or use keyword arguments syntax.
# But actually it does in 2.7.0 and master!
h = {}
p target(**{a: 1}) # => [{:a=>1}]
p target(**h)     # => [] in 2.7.0 and master (inconsistent with above), [{}] in 2.6 and ruby2_keywords by default
p target(h)       # => [{}]

We need to remove empty keyword arguments for one reason: to make *args, **kwargs delegation work with a target method not taking kwargs:

def target(a, b = nil)
  [a, b]
end

def delegate(*args, **kwargs)
  target(*args, **kwargs)
end

p delegate(1) # => [1, {}] in 2.6.5, [1, nil] in 2.7.0, master and ruby2_keywords by default

Note that if target does accept keyword arguments, it would not change anything whether we pass or not an empty keyword Hash in Ruby 3.0+.

Here are two ideas to make *args, **kwargs delegation work, but not remove empty keyword hashes when passed to a method not accepting keyword arguments:

  • When the keyword Hash is created out of nothing, i.e., by def m(**hash_created_by_double_splat) and m was not passed any keyword argument, we could remember that, and when calling another method with foo(**hash_created_by_double_splat) remove that Hash, since the user never intended it. For other cases, keep to pass the keyword Hash just like for non-empty keyword hashes, since the user passed keywords explicitly.
  • Maybe ruby2_keywords-flagged Hash should never be removed. I need to think more about that case and experiment with it.

jeremyevans0 (Jeremy Evans) mame (Yusuke Endoh) What do you think of that?

Updated by jeremyevans0 (Jeremy Evans) 6 months ago

Eregon (Benoit Daloze) wrote:

jeremyevans0 (Jeremy Evans) wrote:

I think matz (Yukihiro Matsumoto) should strongly consider this particular problem when deciding whether to accept ruby2_keywords by default. In Eregon (Benoit Daloze) 's dev meeting presentation, he said of this type of code: "In some rare cases, users might want to treat keyword arguments as positional". Perhaps this type of code is not as rare as once believed.

I believe that is an incorrect conclusion.
This bug happens because ruby2_keywords exists (maybe we can fix **empty_hash or **empty_flagged_hash though, see below).
I was originally against ruby2_keywords because it's not purely lexical and therefore more complicated, but this is a long lost battle now.
Having ruby2_keywords by default changes exactly nothing about this specific bug.
It happens just the same with or without https://github.com/ruby/ruby/pull/2853.

With only explicit ruby2_keywords, you are only affected by the issue if you misuse ruby2_keywords. ruby2_keywords was designed only for handling delegation cases, and usage in non-delegation cases is where it runs into trouble. With ruby2_keywords by default, it's used for all methods accepting args splat and not keywords, making this type of issue much more common.

I consider ruby2_keywords a dangerous method, that should only be used if you know the situation requires it. Not something applied to all methods that would accept it. By applying it to all methods that would accept it, you are introducing dangerous behavior into potentially all ruby programs, and I believe that is a bug.

Eregon (Benoit Daloze) wrote:

To paraphrase Eregon (Benoit Daloze) , if a ruby committer heavily involved in the process cannot figure out even why a problem occurs, let alone the solution, what chance does a standard ruby programmer have?

For the record, by "I don't know why it goes wrong." I meant "This is after midnight and I have no time to investigate it further today".
But yes, I'm surprised a ruby2_keywords flagged Hash is just removed and "lost".
I think we might want to fix that.

I disagree. A ruby2_keywords flagged Hash, when converted to keywords, should be removed if the hash is empty, because that is the same behavior that happens when you double splat an empty hash.

I think the actual issue here might be that we remove empty keyword arguments, which is also inconsistent with non-empty ones when they are given to a method not accepting keyword arguments:

def target(*args)
  args
end

# target does not accept keyword arguments, it shouldn't matter if we pass a last positional Hash or use keyword arguments syntax.
# But actually it does in 2.7.0 and master!
h = {}
p target(**{a: 1}) # => [{:a=>1}]
p target(**h)     # => [] in 2.7.0 and master (inconsistent with above), [{}] in 2.6 and ruby2_keywords by default
p target(h)       # => [{}]

This is by design. Empty keyword splats are equivalent to passing no arguments, just as empty regular splats are equivalent to passing no arguments. I consider the Ruby <2.7 behavior a bug in this regard. When you consider that **{} never passed arguments (was removed by the parser), I think the intended behavior for empty keyword splats was not to pass arguments.

We need to remove empty keyword arguments for one reason: to make *args, **kwargs delegation work with a target method not taking kwargs:

def target(a, b = nil)
  [a, b]
end

def delegate(*args, **kwargs)
  target(*args, **kwargs)
end

p delegate(1) # => [1, {}] in 2.6.5, [1, nil] in 2.7.0, master and ruby2_keywords by default

Note that if target does accept keyword arguments, it would not change anything whether we pass or not an empty keyword Hash in Ruby 3.0+.

Here are two ideas to make *args, **kwargs delegation work, but not remove empty keyword hashes when passed to a method not accepting keyword arguments:

  • When the keyword Hash is created out of nothing, i.e., by def m(**hash_created_by_double_splat) and m was not passed any keyword argument, we could remember that, and when calling another method with foo(**hash_created_by_double_splat) remove that Hash, since the user never intended it. For other cases, keep pass the keyword Hash just like for non-empty keyword hashes, since the user passed keywords explicitly.
  • Maybe ruby2_keywords-flagged Hash should never be removed. I need to think more about that case and experiment with it.

jeremyevans0 (Jeremy Evans) mame (Yusuke Endoh) What do you think of that?

I think the current behavior of always removing empty keyword splats (including implicit splats of empty flagged hashes) makes much more sense.

#7

Updated by jeremyevans (Jeremy Evans) 6 months ago

  • Status changed from Open to Closed

Applied in changeset git|28d31ead34baff1c4abc0d7d902ef4bc1d576fb2.


Fix pp when passed a empty ruby2_keywords-flagged hash as array element

This causes problems because the hash is passed to a block not
accepting keywords. Because the hash is empty and keyword flagged,
it is removed before calling the block. This doesn't cause an
ArgumentError because it is a block and not a lambda. Just like
any other block not passed required arguments, arguments not
passed are set to nil.

Issues like this are a strong reason not to have ruby2_keywords
by default.

Fixes [Bug #16519]

Updated by Eregon (Benoit Daloze) 6 months ago

jeremyevans0 (Jeremy Evans) wrote:

With ruby2_keywords by default, it's used for all methods accepting args splat and not keywords, making this type of issue much more common.

Do you have any existing real code example where that branch causes problems?
I agree it is more likely to have a few problems similar to this one than explicit ruby2_keywords.
OTOH, it would also make migrating thousands of delegation calls (i.e., all delegation cases passing kwargs) so much easier.
It's a trade-off. I'd take one case like this needing , **{}) over finding where to add 1000 explicit ruby2_keywords any day.

BTW, this case is interesting, because we cannot say "this is a misuse of ruby2_keywords".
Printing arguments for debugging with pp should work of course.
It's really a case unique-of-its-kind that way.
In almost all other cases (>99%), passing *args around has the intention to delegate keyword arguments as-is in Ruby 2.6 code.

This is by design. Empty keyword splats are equivalent to passing no arguments, just as empty regular splats are equivalent to passing no arguments. I consider the Ruby <2.7 behavior a bug in this regard. When you consider that **{} never passed arguments (was removed by the parser), I think the intended behavior for empty keyword splats was not to pass arguments.

Yes, I understand this point of view. But the fact is when calling a method not accepting keyword arguments, with syntactic keywords, there is inconsistency between empty keyword arguments and non-empty as shown above.
That's clearly not ideal and makes thinking about ** complicated because it might pass a positional argument or not.
It's something we could have avoided by raising if the method does not accept keyword arguments (but that's probably too incompatible).

I think the current behavior of always removing empty keyword splats (including implicit splats of empty flagged hashes) makes much more sense.

It's also the only construct in Ruby where a Hash argument can fully disappear.
I'd argue if the user passed keyword arguments (e.g., with **h), even empty they should be preserved and not ignored, especially when calling a method not accepting keyword arguments.
I think removing it is counter-intuitive in some cases: for example it causes these weird cases for p(**empty_hash) which prints nothing when p(**non_empty) prints as expected.

Updated by jeremyevans0 (Jeremy Evans) 6 months ago

Eregon (Benoit Daloze) wrote:

jeremyevans0 (Jeremy Evans) wrote:

With ruby2_keywords by default, it's used for all methods accepting args splat and not keywords, making this type of issue much more common.

Do you have any existing real code example where that branch causes problems?

Do you mean other than the example in this ticket? No. Do I expect other real world examples like this one exist? Yes.

I agree it is more likely to have a few problems similar to this one than explicit ruby2_keywords.
OTOH, it would also make migrating thousands of delegation calls (i.e., all delegation cases passing kwargs) so much easier.
It's a trade-off. I'd take one case like this needing , **{}) over finding where to add 1000 explicit ruby2_keywords any day.

It is a trade-off. I agree that in the majority of cases, ruby2_keywords by default does what you want. However, I don't think that's worth the cases where it doesn't, such as this one. A codebase that does not use ruby2_keywords would never be subject to these issues, but if you have ruby2_keywords by default, you can be subject to these issues even if you never use ruby2_keywords in any code.

This is by design. Empty keyword splats are equivalent to passing no arguments, just as empty regular splats are equivalent to passing no arguments. I consider the Ruby <2.7 behavior a bug in this regard. When you consider that **{} never passed arguments (was removed by the parser), I think the intended behavior for empty keyword splats was not to pass arguments.

Yes, I understand this point of view. But the fact is when calling a method not accepting keyword arguments, with syntactic keywords, there is inconsistency between empty keyword arguments and non-empty as shown above.
That's clearly not ideal and makes thinking about ** complicated because it might pass a positional argument or not.
It's something we could have avoided by raising if the method does not accept keyword arguments (but that's probably too incompatible).

I disagree that it is "clearly not ideal". I think it makes the most sense and is the ideal behavior. Conceptually, an empty keyword splat is no arguments, unlike an empty positional hash, which is a single empty argument. Likewise, a non-empty keyword splat is some arguments. For a method that does not accept keywords, any keyword arguments are combined into a single hash, if any keyword arguments are provided. The following two cases should be treated the same (assume foo does not accept keywords):

foo()
foo(**{})

In both cases, no keywords are passed to foo.

I think the current behavior of always removing empty keyword splats (including implicit splats of empty flagged hashes) makes much more sense.

It's also the only construct in Ruby where a Hash argument can fully disappear.

When you do foo(**{}), you can consider the hash disappearing. So treating foo(*[empty_flagged_hash]) the same is consistent.

I'd argue if the user passed keyword arguments (e.g., with **h), even empty they should be preserved and not ignored, especially when calling a method not accepting keyword arguments.
I think removing it is counter-intuitive in some cases: for example it causes these weird cases for p(**empty_hash) which prints nothing when p(**non_empty) prints as expected.

p(**empty_hash) prints a newline, the same as p(), which is what I would expect when passing no keyword arguments.

Updated by Dan0042 (Daniel DeLorme) 6 months ago

FWIW, this case works fine without any change to pp in my proof of concept branch

def flag(*a); a.last; end
pp [flag(**{})]
#=> [{}]  no warnings

Updated by Dan0042 (Daniel DeLorme) 5 months ago

Eregon (Benoit Daloze) wrote in #note-5:

  • When the keyword Hash is created out of nothing, i.e., by def m(**hash_created_by_double_splat) and m was not passed any keyword argument, we could remember that, and when calling another method with foo(**hash_created_by_double_splat) remove that Hash, since the user never intended it. For other cases, keep to pass the keyword Hash just like for non-empty keyword hashes, since the user passed keywords explicitly.

That's the same approach I used in my proof of concept branch, and I find it works pretty well in practice.


jeremyevans0 (Jeremy Evans) wrote in #note-6:

This is by design. Empty keyword splats are equivalent to passing no arguments, just as empty regular splats are equivalent to passing no arguments. I consider the Ruby <2.7 behavior a bug in this regard. When you consider that **{} never passed arguments (was removed by the parser), I think the intended behavior for empty keyword splats was not to pass arguments.

When you say "passing no arguments" I think you're conflating keyword and positional. An empty hash splat should pass no keyword arguments, just as an empty array splat should pass no positional arguments. So I would agree with you IF this was complete and strict separation of keyword/positional, and then we'd get this:

def foo(a=42); a; end
foo(**{})    #=> 42
foo(**{x:1}) #=> error

But here we're talking about the keyword-to-positional compatibility behavior for methods that have no keyword arguments, the one that you championed in #14183. Since this is for the sake of compatibility, it doesn't make sense to introduce incompatibility here. It's also very inconsistent to mix "strict" behavior for empty hashes and compatibility behavior for non-empty. Consider these bugs:

def foo(opts); opts; end
h = {}
h[:k] = 1 if condition
p foo(**h)  #=> {:k=>1} if condition is true
            #=> warning (and eventually error) if condition is false

def bar(*args)
  opts = args.pop if args.last.is_a?(Hash)
  args
end
data = {"k"=>42}
options = {}
bar(data, **options) #=> [] if options is empty; data was taken as opts!
options = {d: 5}
bar(data, **options) #=> [{"k"=>42}] if options is non-empty

Updated by jeremyevans0 (Jeremy Evans) 5 months ago

Dan0042 (Daniel DeLorme) wrote in #note-11:

But here we're talking about the keyword-to-positional compatibility behavior, the one that you championed in #14183. Since this is for the sake of compatibility, it doesn't make sense to introduce incompatibility here. It's also very inconsistent to mix "strict" behavior for empty hashes and compatibility behavior for non-empty. Consider these bugs:

def foo(opts); opts; end
h = {}
h[:k] = 1 if condition
p foo(**h)  #=> {:k=>1} if condition is true
            #=> warning (and eventually error) if condition is false

def bar(*args)
  opts = args.pop if args.last.is_a?(Hash)
  args
end
data = {"k"=>42}
options = {d:5}
bar(data, **options) #=> [] if options is empty; data was taken as opts!
bar(data, **options) #=> [{"k"=>42}] if options is non-empty

The compatibility for treating keywords as a last positional hash is designed to allow code such as:

def foo(opts={}) opts end
foo()
foo(opts)
foo(a: 1)

to continue to work. This type of code is all over the place in Ruby. It's rare you would have a method take a mandatory options hash for keywords ("mandatory options"?), and rarer still that you would pass keywords to it using a double splat.

Having a double splatted empty hash be removed is necessary for delegation to work correctly, and it makes more sense to keep the behavior consistent regardless of how the hash is created. Additionally, the behavior is more consistent with explicit keywords:

def foo(opts) opts end
foo()         # error
foo(**{})     # error
foo(a: 1)     # {a: 1}
foo(**{a: 1}) # {a: 1}

In my mind, if you have a mandatory argument, and you don't pass an argument to it, it should be an ArgumentError. This is the same for regular splats:

def foo(opts) opts end
foo(*[])       # error
foo(**{})      # error
foo(*[{a: 1}]) # {a: 1}
foo(**{a: 1})  # {a: 1}

Regarding your example with bar, the method will treat a positional hash argument as an options hash anyway, so it's basically broken in that regard. Such methods are best to convert to keyword arguments if you want to be able to differentiate positional arguments from keywords/options. Such code was probably written before keyword arguments, and it is unlikely that a double splat would be used when calling such methods.

Updated by Dan0042 (Daniel DeLorme) 5 months ago

jeremyevans0 (Jeremy Evans) wrote in #note-12:

Having a double splatted empty hash be removed is necessary for delegation to work correctly

Except that, no, it's not necessary, and I have the proof of concept to prove it.

Additionally, the behavior is more consistent with explicit keywords:

I agree it's more consistent if an empty hash splat always behaves the same way, but in my experiment I've found this inconsistency to be surprisingly non-confusing. "there is a non-neglectable gap between what is "consistent" for computers, and what is "consistent" for humans." -- zverok

Also, it looks convincing when you use an empty splat literal like **{} in your examples, but you have to look at cases when splatting an empty hash is actually going to happen. There's delegation of course, and there's also the case of building an options hash based on conditions, as shown above. If you have foo(**hash) and you can't predict the behavior because it depends on the hash contents, I consider that a problem worse than inconsistency.

In my mind, if you have a mandatory argument, and you don't pass an argument to it, it should be an ArgumentError. This is the same for regular splats:

In my mind, if you have a mandatory keyword argument, and you don't pass a keyword argument to it, it should be an ArgumentError (and it is). An empy kw-splat is equivalent to "no keyword argument", but not to "no positional argument". Because keyword and positional are separated. When passing a kw-splat to a positional-only method, you're not really passing keywords, but a positional Hash; it should be like ignoring the kw-splat part (but dup'ing the hash) because the method doesn't support keywords.

I've shown behavior that works in 2.6 and breaks in 2.7, but your response seems to be that it doesn't matter, because it's "unlikely" to happen or the code was already "basically broken" (even though it worked in the example). I must say I find that very frustrating; it's not possible to make an argument if all those demonstrated incompatibilities don't even matter or exist in your eyes. :-/

(btw thanks for your replies; I am frustrated but grateful)

Updated by naruse (Yui NARUSE) 4 months ago

  • Backport changed from 2.5: UNKNOWN, 2.6: UNKNOWN, 2.7: REQUIRED to 2.5: UNKNOWN, 2.6: UNKNOWN, 2.7: DONE

ruby_2_7 e85d49e4a91a3ca007468a23d8568f7f886e7e5b.

Updated by naruse (Yui NARUSE) 4 months ago

  • Backport changed from 2.5: UNKNOWN, 2.6: UNKNOWN, 2.7: DONE to 2.5: UNKNOWN, 2.6: UNKNOWN, 2.7: REQUIRED

Just merging 28d31ead34baff1c4abc0d7d902ef4bc1d576fb2 into ruby_2_7 causes test failure

Updated by Eregon (Benoit Daloze) 4 months ago

What's the failure?

Updated by naruse (Yui NARUSE) 4 months ago

  1) Failure:
PPTestModule::PPCycleTest#test_hash [/Users/naruse/work/ruby_2_7/test/test_pp.rb:133]:
<"{0=>{...}}\n"> expected but was
<"{[0, {...}]=>{}}\n">.

  2) Failure:
PPTestModule::PPSingleLineTest#test_hash [/Users/naruse/work/ruby_2_7/test/test_pp.rb:176]:
<"{1=>1}"> expected but was
<"{[1, 1]=>{}}">.

Updated by Eregon (Benoit Daloze) 4 months ago

  • Assignee set to jeremyevans0 (Jeremy Evans)

jeremyevans0 (Jeremy Evans) Could you take a look for the 2.7 backport?

Updated by jeremyevans0 (Jeremy Evans) 4 months ago

Eregon (Benoit Daloze) wrote in #note-18:

jeremyevans0 (Jeremy Evans) Could you take a look for the 2.7 backport?

Sure, I'll take a look tomorrow and see if I can fix it locally and submit a pull request for the ruby_2_7 branch.

Updated by jeremyevans0 (Jeremy Evans) 4 months ago

jeremyevans0 (Jeremy Evans) wrote in #note-19:

Eregon (Benoit Daloze) wrote in #note-18:

jeremyevans0 (Jeremy Evans) Could you take a look for the 2.7 backport?

Sure, I'll take a look tomorrow and see if I can fix it locally and submit a pull request for the ruby_2_7 branch.

Pull requested submitted: https://github.com/ruby/ruby/pull/2966

Things need to be handled differently in 2.7 as 2.7 will do empty keyword splat to required positional argument conversion.

Also available in: Atom PDF