Support keyword splatting nil

nil is treated similarly to the empty hash in this case, passing
no keywords and not calling any conversion methods.

Fixes [Bug #20064]

Co-authored-by: Nobuyoshi Nakada <nobu@ruby-lang.org>
This commit is contained in:
Jeremy Evans 2024-01-14 11:41:02 -08:00 committed by GitHub
parent 772413245f
commit 5c823aa686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 60 additions and 7 deletions

View File

@ -9,6 +9,11 @@ Note that each entry is kept to a minimum, see links for details.
* `it` is added to reference a block parameter. [[Feature #18980]]
* Keyword splatting `nil` when calling methods is now supported.
`**nil` is treated similar to `**{}`, passing no keywords,
and not calling any conversion methods.
[[Bug #20064]]
## Core classes updates
Note: We're only listing outstanding class updates.
@ -57,3 +62,4 @@ See GitHub releases like [GitHub Releases of Logger](https://github.com/ruby/log
## JIT
[Feature #18980]: https://bugs.ruby-lang.org/issues/18980
[Bug #20064]: https://bugs.ruby-lang.org/issues/20064

View File

@ -5079,6 +5079,7 @@ compile_hash(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, int meth
int last_kw = !RNODE_LIST(RNODE_LIST(node)->nd_next)->nd_next; /* foo( ..., **kw) */
int only_kw = last_kw && first_kw; /* foo(1,2,3, **kw) */
empty_kw = empty_kw || nd_type_p(kw, NODE_NIL); /* foo( ..., **nil, ...) */
if (empty_kw) {
if (only_kw && method_call_keywords) {
/* **{} appears at the only keyword argument in method call,

View File

@ -378,7 +378,7 @@ class TestBacktrace < Test::Unit::TestCase
def test_core_backtrace_hash_merge
e = assert_raise(TypeError) do
{**nil}
{**1}
end
assert_not_match(/\Acore#/, e.backtrace_locations[0].base_label)
end

View File

@ -182,6 +182,44 @@ class TestKeywordArguments < Test::Unit::TestCase
[:keyrest, :kw], [:block, :b]], method(:f9).parameters)
end
def test_keyword_splat_nil
# cfunc call
assert_equal(nil, p(**nil))
def self.a0; end
assert_equal(nil, a0(**nil))
assert_equal(nil, :a0.to_proc.call(self, **nil))
def self.o(x=1); x end
assert_equal(1, o(**nil))
assert_equal(2, o(2, **nil))
assert_equal(1, o(*nil, **nil))
assert_equal(1, o(**nil, **nil))
assert_equal({a: 1}, o(a: 1, **nil))
assert_equal({a: 1}, o(**nil, a: 1))
# symproc call
assert_equal(1, :o.to_proc.call(self, **nil))
def self.s(*a); a end
assert_equal([], s(**nil))
assert_equal([1], s(1, **nil))
assert_equal([], s(*nil, **nil))
def self.kws(**a); a end
assert_equal({}, kws(**nil))
assert_equal({}, kws(*nil, **nil))
def self.skws(*a, **kw); [a, kw] end
assert_equal([[], {}], skws(**nil))
assert_equal([[1], {}], skws(1, **nil))
assert_equal([[], {}], skws(*nil, **nil))
assert_equal({}, {**nil})
assert_equal({a: 1}, {a: 1, **nil})
assert_equal({a: 1}, {**nil, a: 1})
end
def test_lambda
f = ->(str: "foo", num: 424242) { [str, num] }
assert_equal(["foo", 424242], f[])

4
vm.c
View File

@ -3666,7 +3666,9 @@ kwmerge_i(VALUE key, VALUE value, VALUE hash)
static VALUE
m_core_hash_merge_kwd(VALUE recv, VALUE hash, VALUE kw)
{
REWIND_CFP(hash = core_hash_merge_kwd(hash, kw));
if (!NIL_P(kw)) {
REWIND_CFP(hash = core_hash_merge_kwd(hash, kw));
}
return hash;
}

View File

@ -434,6 +434,10 @@ fill_keys_values(st_data_t key, st_data_t val, st_data_t ptr)
static inline int
ignore_keyword_hash_p(VALUE keyword_hash, const rb_iseq_t * const iseq, unsigned int * kw_flag, VALUE * converted_keyword_hash)
{
if (keyword_hash == Qnil) {
return 1;
}
if (!RB_TYPE_P(keyword_hash, T_HASH)) {
keyword_hash = rb_to_hash_type(keyword_hash);
}

View File

@ -2668,8 +2668,10 @@ static inline VALUE
vm_caller_setup_keyword_hash(const struct rb_callinfo *ci, VALUE keyword_hash)
{
if (UNLIKELY(!RB_TYPE_P(keyword_hash, T_HASH))) {
/* Convert a non-hash keyword splat to a new hash */
keyword_hash = rb_hash_dup(rb_to_hash_type(keyword_hash));
if (keyword_hash != Qnil) {
/* Convert a non-hash keyword splat to a new hash */
keyword_hash = rb_hash_dup(rb_to_hash_type(keyword_hash));
}
}
else if (!IS_ARGS_KW_SPLAT_MUT(ci)) {
/* Convert a hash keyword splat to a new hash unless
@ -2699,7 +2701,7 @@ CALLER_SETUP_ARG(struct rb_control_frame_struct *restrict cfp,
if (vm_caller_setup_arg_splat(cfp, calling, ary, max_args)) return;
// put kw
if (!RHASH_EMPTY_P(kwh)) {
if (kwh != Qnil && !RHASH_EMPTY_P(kwh)) {
if (UNLIKELY(calling->heap_argv)) {
rb_ary_push(calling->heap_argv, kwh);
((struct RHash *)kwh)->basic.flags |= RHASH_PASS_AS_KEYWORDS;
@ -2770,7 +2772,7 @@ check_keyword:
VM_ASSERT(calling->kw_splat == 1);
VALUE kwh = vm_caller_setup_keyword_hash(ci, cfp->sp[-1]);
if (RHASH_EMPTY_P(kwh)) {
if (kwh == Qnil || RHASH_EMPTY_P(kwh)) {
cfp->sp--;
calling->argc--;
calling->kw_splat = 0;
@ -3596,7 +3598,7 @@ vm_call_cfunc_only_splat_kw(rb_execution_context_t *ec, rb_control_frame_t *reg_
RB_DEBUG_COUNTER_INC(ccf_cfunc_only_splat_kw);
VALUE keyword_hash = reg_cfp->sp[-1];
if (RB_TYPE_P(keyword_hash, T_HASH) && RHASH_EMPTY_P(keyword_hash)) {
if (keyword_hash == Qnil || (RB_TYPE_P(keyword_hash, T_HASH) && RHASH_EMPTY_P(keyword_hash))) {
return vm_call_cfunc_array_argv(ec, reg_cfp, calling, 1, 0);
}