diff --git a/internal.h b/internal.h index bb298d2bfb..7de0077d86 100644 --- a/internal.h +++ b/internal.h @@ -815,6 +815,7 @@ struct RComplex { #define RCOMPLEX_SET_IMAG(cmp, i) RB_OBJ_WRITE((cmp), &((struct RComplex *)(cmp))->imag,(i)) enum ruby_rhash_flags { + RHASH_PASS_AS_KEYWORDS = FL_USER1, /* FL 1 */ RHASH_PROC_DEFAULT = FL_USER2, /* FL 2 */ RHASH_ST_TABLE_FLAG = FL_USER3, /* FL 3 */ #define RHASH_AR_TABLE_MAX_SIZE SIZEOF_VALUE diff --git a/lib/delegate.rb b/lib/delegate.rb index 03ebfddf4a..a1589ecd08 100644 --- a/lib/delegate.rb +++ b/lib/delegate.rb @@ -75,7 +75,7 @@ class Delegator < BasicObject # # Handles the magic of delegation through \_\_getobj\_\_. # - def method_missing(m, *args, &block) + ruby2_keywords def method_missing(m, *args, &block) r = true target = self.__getobj__ {r = false} diff --git a/test/ruby/test_keyword.rb b/test/ruby/test_keyword.rb index 1dbde80cd5..1cfa982f0c 100644 --- a/test/ruby/test_keyword.rb +++ b/test/ruby/test_keyword.rb @@ -2306,6 +2306,278 @@ class TestKeywordArguments < Test::Unit::TestCase assert_raise(ArgumentError) { m.call(42, a: 1, **h2) } end + def test_ruby2_keywords + c = Class.new do + ruby2_keywords def foo(meth, *args) + send(meth, *args) + end + + ruby2_keywords def foo_bar(*args) + bar(*args) + end + + ruby2_keywords def foo_baz(*args) + baz(*args) + end + + ruby2_keywords def foo_mod(meth, *args) + args << 1 + send(meth, *args) + end + + ruby2_keywords def foo_bar_mod(*args) + args << 1 + bar(*args) + end + + ruby2_keywords def foo_baz_mod(*args) + args << 1 + baz(*args) + end + + def bar(*args, **kw) + [args, kw] + end + + def baz(*args) + args + end + + ruby2_keywords def foo_dbar(*args) + dbar(*args) + end + + ruby2_keywords def foo_dbaz(*args) + dbaz(*args) + end + + define_method(:dbar) do |*args, **kw| + [args, kw] + end + + define_method(:dbaz) do |*args| + args + end + + ruby2_keywords def block(*args) + ->(*args, **kw){[args, kw]}.(*args) + end + + ruby2_keywords def cfunc(*args) + self.class.new(*args).init_args + end + + ruby2_keywords def store_foo(meth, *args) + @stored_args = args + use(meth) + end + def use(meth) + send(meth, *@stored_args) + end + + attr_reader :init_args + def initialize(*args, **kw) + @init_args = [args, kw] + end + end + + mmkw = Class.new do + def method_missing(*args, **kw) + [args, kw] + end + end + + mmnokw = Class.new do + def method_missing(*args) + args + end + end + + implicit_super = Class.new(c) do + ruby2_keywords def bar(*args) + super + end + + ruby2_keywords def baz(*args) + super + end + end + + explicit_super = Class.new(c) do + ruby2_keywords def bar(*args) + super(*args) + end + + ruby2_keywords def baz(*args) + super(*args) + end + end + + h1 = {a: 1} + o = c.new + + assert_equal([[1], h1], o.foo(:bar, 1, :a=>1)) + assert_equal([1, h1], o.foo(:baz, 1, :a=>1)) + assert_equal([[1], h1], o.store_foo(:bar, 1, :a=>1)) + assert_equal([1, h1], o.store_foo(:baz, 1, :a=>1)) + assert_equal([[1], h1], o.foo_bar(1, :a=>1)) + assert_equal([1, h1], o.foo_baz(1, :a=>1)) + + assert_equal([[1], h1], o.foo(:bar, 1, **h1)) + assert_equal([1, h1], o.foo(:baz, 1, **h1)) + assert_equal([[1], h1], o.store_foo(:bar, 1, **h1)) + assert_equal([1, h1], o.store_foo(:baz, 1, **h1)) + assert_equal([[1], h1], o.foo_bar(1, **h1)) + assert_equal([1, h1], o.foo_baz(1, **h1)) + + assert_equal([[h1], {}], o.foo(:bar, h1, **{})) + assert_equal([h1], o.foo(:baz, h1, **{})) + assert_equal([[h1], {}], o.store_foo(:bar, h1, **{})) + assert_equal([h1], o.store_foo(:baz, h1, **{})) + assert_equal([[h1], {}], o.foo_bar(h1, **{})) + assert_equal([h1], o.foo_baz(h1, **{})) + + assert_warn(/The last argument is used as the keyword parameter.* for `bar'/m) do + assert_equal([[1], h1], o.foo(:bar, 1, h1)) + end + assert_equal([1, h1], o.foo(:baz, 1, h1)) + assert_warn(/The last argument is used as the keyword parameter.* for `bar'/m) do + assert_equal([[1], h1], o.store_foo(:bar, 1, h1)) + end + assert_equal([1, h1], o.store_foo(:baz, 1, h1)) + assert_warn(/The last argument is used as the keyword parameter.* for `bar'/m) do + assert_equal([[1], h1], o.foo_bar(1, h1)) + end + assert_equal([1, h1], o.foo_baz(1, h1)) + + assert_equal([[1, h1, 1], {}], o.foo_mod(:bar, 1, :a=>1)) + assert_equal([1, h1, 1], o.foo_mod(:baz, 1, :a=>1)) + assert_equal([[1, h1, 1], {}], o.foo_bar_mod(1, :a=>1)) + assert_equal([1, h1, 1], o.foo_baz_mod(1, :a=>1)) + + assert_equal([[1, h1, 1], {}], o.foo_mod(:bar, 1, **h1)) + assert_equal([1, h1, 1], o.foo_mod(:baz, 1, **h1)) + assert_equal([[1, h1, 1], {}], o.foo_bar_mod(1, **h1)) + assert_equal([1, h1, 1], o.foo_baz_mod(1, **h1)) + + assert_equal([[h1, {}, 1], {}], o.foo_mod(:bar, h1, **{})) + assert_equal([h1, {}, 1], o.foo_mod(:baz, h1, **{})) + assert_equal([[h1, {}, 1], {}], o.foo_bar_mod(h1, **{})) + assert_equal([h1, {}, 1], o.foo_baz_mod(h1, **{})) + + assert_equal([[1, h1, 1], {}], o.foo_mod(:bar, 1, h1)) + assert_equal([1, h1, 1], o.foo_mod(:baz, 1, h1)) + assert_equal([[1, h1, 1], {}], o.foo_bar_mod(1, h1)) + assert_equal([1, h1, 1], o.foo_baz_mod(1, h1)) + + assert_equal([[1], h1], o.foo(:dbar, 1, :a=>1)) + assert_equal([1, h1], o.foo(:dbaz, 1, :a=>1)) + assert_equal([[1], h1], o.store_foo(:dbar, 1, :a=>1)) + assert_equal([1, h1], o.store_foo(:dbaz, 1, :a=>1)) + assert_equal([[1], h1], o.foo_dbar(1, :a=>1)) + assert_equal([1, h1], o.foo_dbaz(1, :a=>1)) + + assert_equal([[1], h1], o.foo(:dbar, 1, **h1)) + assert_equal([1, h1], o.foo(:dbaz, 1, **h1)) + assert_equal([[1], h1], o.store_foo(:dbar, 1, **h1)) + assert_equal([1, h1], o.store_foo(:dbaz, 1, **h1)) + assert_equal([[1], h1], o.foo_dbar(1, **h1)) + assert_equal([1, h1], o.foo_dbaz(1, **h1)) + + assert_equal([[h1], {}], o.foo(:dbar, h1, **{})) + assert_equal([h1], o.foo(:dbaz, h1, **{})) + assert_equal([[h1], {}], o.store_foo(:dbar, h1, **{})) + assert_equal([h1], o.store_foo(:dbaz, h1, **{})) + assert_equal([[h1], {}], o.foo_dbar(h1, **{})) + assert_equal([h1], o.foo_dbaz(h1, **{})) + + assert_warn(/The last argument is used as the keyword parameter.* for method/m) do + assert_equal([[1], h1], o.foo(:dbar, 1, h1)) + end + assert_equal([1, h1], o.foo(:dbaz, 1, h1)) + assert_warn(/The last argument is used as the keyword parameter.* for method/m) do + assert_equal([[1], h1], o.store_foo(:dbar, 1, h1)) + end + assert_equal([1, h1], o.store_foo(:dbaz, 1, h1)) + assert_warn(/The last argument is used as the keyword parameter.* for method/m) do + assert_equal([[1], h1], o.foo_dbar(1, h1)) + end + assert_equal([1, h1], o.foo_dbaz(1, h1)) + + assert_equal([[1], h1], o.block(1, :a=>1)) + assert_equal([[1], h1], o.block(1, **h1)) + assert_warn(/The last argument is used as the keyword parameter.* for `call'/m) do + assert_equal([[1], h1], o.block(1, h1)) + end + assert_equal([[h1], {}], o.block(h1, **{})) + + assert_equal([[1], h1], o.cfunc(1, :a=>1)) + assert_equal([[1], h1], o.cfunc(1, **h1)) + assert_warn(/The last argument is used as the keyword parameter.* for `initialize'/m) do + assert_equal([[1], h1], o.cfunc(1, h1)) + end + assert_equal([[h1], {}], o.cfunc(h1, **{})) + + o = mmkw.new + assert_equal([[:b, 1], h1], o.b(1, :a=>1)) + assert_equal([[:b, 1], h1], o.b(1, **h1)) + assert_warn(/The last argument is used as the keyword parameter.* for `method_missing'/m) do + assert_equal([[:b, 1], h1], o.b(1, h1)) + end + assert_equal([[:b, h1], {}], o.b(h1, **{})) + + o = mmnokw.new + assert_equal([:b, 1, h1], o.b(1, :a=>1)) + assert_equal([:b, 1, h1], o.b(1, **h1)) + assert_equal([:b, 1, h1], o.b(1, h1)) + assert_equal([:b, h1], o.b(h1, **{})) + + o = implicit_super.new + assert_equal([[1], h1], o.bar(1, :a=>1)) + assert_equal([[1], h1], o.bar(1, **h1)) + assert_warn(/The last argument is used as the keyword parameter.* for `bar'/m) do + assert_equal([[1], h1], o.bar(1, h1)) + end + assert_equal([[h1], {}], o.bar(h1, **{})) + + assert_equal([1, h1], o.baz(1, :a=>1)) + assert_equal([1, h1], o.baz(1, **h1)) + assert_equal([1, h1], o.baz(1, h1)) + assert_equal([h1], o.baz(h1, **{})) + + o = explicit_super.new + assert_equal([[1], h1], o.bar(1, :a=>1)) + assert_equal([[1], h1], o.bar(1, **h1)) + assert_warn(/The last argument is used as the keyword parameter.* for `bar'/m) do + assert_equal([[1], h1], o.bar(1, h1)) + end + assert_equal([[h1], {}], o.bar(h1, **{})) + + assert_equal([1, h1], o.baz(1, :a=>1)) + assert_equal([1, h1], o.baz(1, **h1)) + assert_equal([1, h1], o.baz(1, h1)) + assert_equal([h1], o.baz(h1, **{})) + + assert_warn(/Skipping set of ruby2_keywords flag for bar \(method not defined in Ruby, method accepts keywords, or method does not accept argument splat\)/) do + assert_nil(c.send(:ruby2_keywords, :bar)) + end + + sc = Class.new(c) + assert_warn(/Skipping set of ruby2_keywords flag for bar \(can only set in method defining module\)/) do + sc.send(:ruby2_keywords, :bar) + end + m = Module.new + assert_warn(/Skipping set of ruby2_keywords flag for system \(can only set in method defining module\)/) do + m.send(:ruby2_keywords, :system) + end + + assert_raise(NameError) { c.send(:ruby2_keywords, "a5e36ccec4f5080a1d5e63f8") } + assert_raise(NameError) { c.send(:ruby2_keywords, :quux) } + + c.freeze + assert_raise(FrozenError) { c.send(:ruby2_keywords, :baz) } + end + def test_dig_kwsplat kw = {} h = {:a=>1} diff --git a/test/test_delegate.rb b/test/test_delegate.rb index 38e38ad781..9634681797 100644 --- a/test/test_delegate.rb +++ b/test/test_delegate.rb @@ -177,6 +177,25 @@ class TestDelegateClass < Test::Unit::TestCase assert_not_operator(s0, :eql?, "bar") end + def test_keyword_and_hash + foo = Object.new + def foo.bar(*args) + args + end + def foo.foo(*args, **kw) + [args, kw] + end + d = SimpleDelegator.new(foo) + assert_equal([[], {}], d.foo) + assert_equal([], d.bar) + assert_equal([[], {:a=>1}], d.foo(:a=>1)) + assert_equal([{:a=>1}], d.bar(:a=>1)) + assert_warn(/The last argument is used as the keyword parameter.* for `foo'/m) do + assert_equal([[], {:a=>1}], d.foo({:a=>1})) + end + assert_equal([{:a=>1}], d.bar({:a=>1})) + end + class Foo private def delegate_test_private diff --git a/vm_args.c b/vm_args.c index b235072d32..d128f91fbc 100644 --- a/vm_args.c +++ b/vm_args.c @@ -671,6 +671,8 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co VALUE keyword_hash = Qnil; VALUE * const orig_sp = ec->cfp->sp; unsigned int i; + int remove_empty_keyword_hash = 1; + VALUE flag_keyword_hash = 0; vm_check_canary(ec, orig_sp); /* @@ -720,41 +722,79 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co args->kw_argv = NULL; } + if (kw_flag && iseq->body->param.flags.ruby2_keywords) { + remove_empty_keyword_hash = 0; + } + if (ci->flag & VM_CALL_ARGS_SPLAT) { + VALUE rest_last = 0; + int len; args->rest = locals[--args->argc]; args->rest_index = 0; - given_argc += RARRAY_LENINT(args->rest) - 1; + len = RARRAY_LENINT(args->rest); + given_argc += len - 1; + rest_last = RARRAY_AREF(args->rest, len - 1); + + if (!kw_flag && len > 0) { + if (RB_TYPE_P(rest_last, T_HASH) && + (((struct RHash *)rest_last)->basic.flags & RHASH_PASS_AS_KEYWORDS)) { + kw_flag |= VM_CALL_KW_SPLAT; + } else { + rest_last = 0; + } + } + if (kw_flag & VM_CALL_KW_SPLAT) { - int len = RARRAY_LENINT(args->rest); if (len > 0 && ignore_keyword_hash_p(RARRAY_AREF(args->rest, len - 1), iseq)) { if (given_argc != min_argc) { - arg_rest_dup(args); - rb_ary_pop(args->rest); - given_argc--; - kw_flag &= ~VM_CALL_KW_SPLAT; + if (remove_empty_keyword_hash) { + arg_rest_dup(args); + rb_ary_pop(args->rest); + given_argc--; + kw_flag &= ~VM_CALL_KW_SPLAT; + } + else { + flag_keyword_hash = rest_last; + } } else { rb_warn_keyword_to_last_hash(calling, ci, iseq); } } + else if (!remove_empty_keyword_hash && rest_last) { + flag_keyword_hash = rest_last; + } } } else { if (kw_flag & VM_CALL_KW_SPLAT) { - if (ignore_keyword_hash_p(args->argv[args->argc-1], iseq)) { + VALUE last_arg = args->argv[args->argc-1]; + if (ignore_keyword_hash_p(last_arg, iseq)) { if (given_argc != min_argc) { - args->argc--; - given_argc--; - kw_flag &= ~VM_CALL_KW_SPLAT; + if (remove_empty_keyword_hash) { + args->argc--; + given_argc--; + kw_flag &= ~VM_CALL_KW_SPLAT; + } + else { + flag_keyword_hash = last_arg; + } } else { rb_warn_keyword_to_last_hash(calling, ci, iseq); } } + else if (!remove_empty_keyword_hash) { + flag_keyword_hash = args->argv[args->argc-1]; + } } args->rest = Qfalse; } + if (flag_keyword_hash && RB_TYPE_P(flag_keyword_hash, T_HASH)) { + ((struct RHash *)flag_keyword_hash)->basic.flags |= RHASH_PASS_AS_KEYWORDS; + } + if (kw_flag && iseq->body->param.flags.accepts_no_kwarg) { rb_raise(rb_eArgError, "no keywords accepted"); } diff --git a/vm_core.h b/vm_core.h index bc7e6bec55..4c233fa27f 100644 --- a/vm_core.h +++ b/vm_core.h @@ -358,6 +358,7 @@ struct rb_iseq_constant_body { unsigned int ambiguous_param0 : 1; /* {|a|} */ unsigned int accepts_no_kwarg : 1; + unsigned int ruby2_keywords: 1; } flags; unsigned int size; diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 56767a4a62..49e865d96f 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -1770,15 +1770,21 @@ rb_iseq_only_kwparam_p(const rb_iseq_t *iseq) static inline void -CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(struct rb_control_frame_struct *restrict cfp, - struct rb_calling_info *restrict calling, - const struct rb_call_info *restrict ci) +CALLER_SETUP_ARG(struct rb_control_frame_struct *restrict cfp, + struct rb_calling_info *restrict calling, + const struct rb_call_info *restrict ci) { if (UNLIKELY(IS_ARGS_SPLAT(ci))) { /* This expands the rest argument to the stack. * So, ci->flag & VM_CALL_ARGS_SPLAT is now inconsistent. */ vm_caller_setup_arg_splat(cfp, calling); + if (!IS_ARGS_KW_OR_KW_SPLAT(ci) && + calling->argc > 0 && + RB_TYPE_P(*(cfp->sp - 1), T_HASH) && + (((struct RHash *)*(cfp->sp - 1))->basic.flags & RHASH_PASS_AS_KEYWORDS)) { + calling->kw_splat = 1; + } } if (UNLIKELY(IS_ARGS_KEYWORD(ci))) { /* This converts VM_CALL_KWARG style to VM_CALL_KW_SPLAT style @@ -1790,12 +1796,10 @@ CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(struct rb_control_frame_struct *restrict cfp, } static inline void -CALLER_SETUP_ARG(struct rb_control_frame_struct *restrict cfp, - struct rb_calling_info *restrict calling, - const struct rb_call_info *restrict ci) +CALLER_REMOVE_EMPTY_KW_SPLAT(struct rb_control_frame_struct *restrict cfp, + struct rb_calling_info *restrict calling, + const struct rb_call_info *restrict ci) { - CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(cfp, calling, ci); - if (UNLIKELY(calling->kw_splat)) { /* This removes the last Hash object if it is empty. * So, ci->flag & VM_CALL_KW_SPLAT is now inconsistent. @@ -1920,6 +1924,7 @@ vm_callee_setup_arg(rb_execution_context_t *ec, struct rb_calling_info *calling, if (LIKELY(rb_simple_iseq_p(iseq))) { rb_control_frame_t *cfp = ec->cfp; CALLER_SETUP_ARG(cfp, calling, ci); + CALLER_REMOVE_EMPTY_KW_SPLAT(cfp, calling, ci); if (calling->argc != iseq->body->param.lead_num) { argument_arity_error(ec, iseq, calling->argc, iseq->body->param.lead_num, iseq->body->param.lead_num); @@ -1931,6 +1936,7 @@ vm_callee_setup_arg(rb_execution_context_t *ec, struct rb_calling_info *calling, else if (rb_iseq_only_optparam_p(iseq)) { rb_control_frame_t *cfp = ec->cfp; CALLER_SETUP_ARG(cfp, calling, ci); + CALLER_REMOVE_EMPTY_KW_SPLAT(cfp, calling, ci); const int lead_num = iseq->body->param.lead_num; const int opt_num = iseq->body->param.opt_num; @@ -2285,10 +2291,12 @@ vm_call_cfunc_with_frame(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp static VALUE vm_call_cfunc(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc) { - int empty_kw_splat = calling->kw_splat; + int empty_kw_splat; RB_DEBUG_COUNTER_INC(ccf_cfunc); CALLER_SETUP_ARG(reg_cfp, calling, ci); + empty_kw_splat = calling->kw_splat; + CALLER_REMOVE_EMPTY_KW_SPLAT(reg_cfp, calling, ci); if (empty_kw_splat && calling->kw_splat) { empty_kw_splat = 0; } @@ -2333,7 +2341,7 @@ vm_call_bmethod(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_c VALUE *argv; int argc; - CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(cfp, calling, ci); + CALLER_SETUP_ARG(cfp, calling, ci); argc = calling->argc; argv = ALLOCA_N(VALUE, argc); MEMCPY(argv, cfp->sp - argc, VALUE, argc); @@ -2363,7 +2371,7 @@ vm_call_opt_send(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct struct rb_call_info_with_kwarg ci_entry; struct rb_call_cache cc_entry, *cc; - CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(reg_cfp, calling, orig_ci); + CALLER_SETUP_ARG(reg_cfp, calling, orig_ci); i = calling->argc - 1; @@ -2468,7 +2476,7 @@ vm_call_method_missing(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_call_cache cc_entry, *cc; unsigned int argc; - CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(reg_cfp, calling, orig_ci); + CALLER_SETUP_ARG(reg_cfp, calling, orig_ci); argc = calling->argc+1; ci_entry.flag = VM_CALL_FCALL | VM_CALL_OPT_SEND | (calling->kw_splat ? VM_CALL_KW_SPLAT : 0); @@ -2673,12 +2681,12 @@ vm_call_method_each_type(rb_execution_context_t *ec, rb_control_frame_t *cfp, st return vm_call_cfunc(ec, cfp, calling, ci, cc); case VM_METHOD_TYPE_ATTRSET: + CALLER_SETUP_ARG(cfp, calling, ci); if (calling->argc == 1 && calling->kw_splat && RHASH_EMPTY_P(cfp->sp[-1])) { - CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(cfp, calling, ci); rb_warn_keyword_to_last_hash(calling, ci, NULL); } else { - CALLER_SETUP_ARG(cfp, calling, ci); + CALLER_REMOVE_EMPTY_KW_SPLAT(cfp, calling, ci); } rb_check_arity(calling->argc, 1, 1); @@ -2688,6 +2696,7 @@ vm_call_method_each_type(rb_execution_context_t *ec, rb_control_frame_t *cfp, st case VM_METHOD_TYPE_IVAR: CALLER_SETUP_ARG(cfp, calling, ci); + CALLER_REMOVE_EMPTY_KW_SPLAT(cfp, calling, ci); rb_check_arity(calling->argc, 0, 0); cc->aux.index = 0; CC_SET_FASTPATH(cc, vm_call_ivar, !(ci->flag & VM_CALL_ARGS_SPLAT)); @@ -2998,12 +3007,12 @@ vm_callee_setup_block_arg(rb_execution_context_t *ec, struct rb_calling_info *ca rb_control_frame_t *cfp = ec->cfp; VALUE arg0; + CALLER_SETUP_ARG(cfp, calling, ci); if (calling->kw_splat && calling->argc == iseq->body->param.lead_num + iseq->body->param.post_num && RHASH_EMPTY_P(cfp->sp[-1])) { - CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(cfp, calling, ci); rb_warn_keyword_to_last_hash(calling, ci, iseq); } else { - CALLER_SETUP_ARG(cfp, calling, ci); + CALLER_REMOVE_EMPTY_KW_SPLAT(cfp, calling, ci); } if (arg_setup_type == arg_setup_block && @@ -3088,7 +3097,7 @@ vm_invoke_symbol_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, { VALUE val; int argc; - CALLER_SETUP_ARG_WITHOUT_KW_SPLAT(ec->cfp, calling, ci); + CALLER_SETUP_ARG(ec->cfp, calling, ci); argc = calling->argc; val = vm_yield_with_symbol(ec, symbol, argc, STACK_ADDR_FROM_TOP(argc), calling->kw_splat, calling->block_handler); POPN(argc); @@ -3104,6 +3113,7 @@ vm_invoke_ifunc_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, int argc; int kw_splat = calling->kw_splat; CALLER_SETUP_ARG(ec->cfp, calling, ci); + CALLER_REMOVE_EMPTY_KW_SPLAT(ec->cfp, calling, ci); if (kw_splat && !calling->kw_splat) { kw_splat = 2; } diff --git a/vm_method.c b/vm_method.c index a3d56c8baf..554d209110 100644 --- a/vm_method.c +++ b/vm_method.c @@ -1745,6 +1745,84 @@ rb_mod_private(int argc, VALUE *argv, VALUE module) return set_visibility(argc, argv, module, METHOD_VISI_PRIVATE); } +/* + * call-seq: + * ruby2_keywords(method_name, ...) -> self + * + * For the given method names, marks the method as passing keywords through + * a normal argument splat. This should only be called on methods that + * accept an argument splat (*args) but not explicit keywords or + * a keyword splat. It marks the method such that if the method is called + * with keyword arguments, the final hash argument is marked with a special + * flag such that if it is the final element of a normal argument splat to + * another method call, and that method calls does not include explicit + * keywords or a keyword splat, the final element is interpreted as keywords. + * In other words, keywords will be passed through the method to other + * methods. + * + * This should only be used for methods that delegate keywords to another + * method, and only for backwards compatibility with Ruby versions before + * 2.7. + * + * This method will probably be removed at some point, as it exists only + * for backwards compatibility, so always check that the module responds + * to this method before calling it. + * + * module Mod + * def foo(meth, *args, &block) + * send(:"do_#{meth}", *args, &block) + * end + * ruby2_keywords(:foo) if respond_to?(:ruby2_keywords, true) + * end + */ + +static VALUE +rb_mod_ruby2_keywords(int argc, VALUE *argv, VALUE module) +{ + int i; + VALUE origin_class = RCLASS_ORIGIN(module); + + rb_check_frozen(module); + + for (i = 0; i < argc; i++) { + VALUE v = argv[i]; + ID name = rb_check_id(&v); + rb_method_entry_t *me; + VALUE defined_class; + + if (!name) { + rb_print_undef_str(module, v); + } + + me = search_method(origin_class, name, &defined_class); + if (!me && RB_TYPE_P(module, T_MODULE)) { + me = search_method(rb_cObject, name, &defined_class); + } + + if (UNDEFINED_METHOD_ENTRY_P(me) || + UNDEFINED_REFINED_METHOD_P(me->def)) { + rb_print_undef(module, name, METHOD_VISI_UNDEF); + } + + if (module == defined_class || origin_class == defined_class) { + if (me->def->type == VM_METHOD_TYPE_ISEQ && + me->def->body.iseq.iseqptr->body->param.flags.has_rest && + !me->def->body.iseq.iseqptr->body->param.flags.has_kw && + !me->def->body.iseq.iseqptr->body->param.flags.has_kwrest) { + me->def->body.iseq.iseqptr->body->param.flags.ruby2_keywords = 1; + rb_clear_method_cache_by_class(module); + } + else { + rb_warn("Skipping set of ruby2_keywords flag for %s (method not defined in Ruby, method accepts keywords, or method does not accept argument splat)", rb_id2name(name)); + } + } + else { + rb_warn("Skipping set of ruby2_keywords flag for %s (can only set in method defining module)", rb_id2name(name)); + } + } + return Qnil; +} + /* * call-seq: * mod.public_class_method(symbol, ...) -> mod @@ -2127,6 +2205,7 @@ Init_eval_method(void) rb_define_private_method(rb_cModule, "protected", rb_mod_protected, -1); rb_define_private_method(rb_cModule, "private", rb_mod_private, -1); rb_define_private_method(rb_cModule, "module_function", rb_mod_modfunc, -1); + rb_define_private_method(rb_cModule, "ruby2_keywords", rb_mod_ruby2_keywords, -1); rb_define_method(rb_cModule, "method_defined?", rb_mod_method_defined, -1); rb_define_method(rb_cModule, "public_method_defined?", rb_mod_public_method_defined, -1);