summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal.h1
-rw-r--r--lib/delegate.rb2
-rw-r--r--test/ruby/test_keyword.rb272
-rw-r--r--test/test_delegate.rb19
-rw-r--r--vm_args.c60
-rw-r--r--vm_core.h1
-rw-r--r--vm_insnhelper.c44
-rw-r--r--vm_method.c79
8 files changed, 450 insertions, 28 deletions
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
@@ -1747,6 +1747,84 @@ rb_mod_private(int argc, VALUE *argv, VALUE module)
/*
* 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 (<tt>*args</tt>) 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
* mod.public_class_method(string, ...) -> 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);