aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeremy Evans <code@jeremyevans.net>2019-09-21 09:03:36 -0700
committerJeremy Evans <code@jeremyevans.net>2019-09-25 12:33:52 -0700
commit3b302ea8c95d34d5ef072d7e3b326f28a611e479 (patch)
tree5a0a5cadb3511d6a3ecf4f234abffecafbeec9d8
parent80b5a0ff2a7709367178f29d4ebe1c54122b1c27 (diff)
downloadruby-3b302ea8c95d34d5ef072d7e3b326f28a611e479.tar.gz
Add Module#ruby2_keywords for passing keywords through regular argument splats
This approach uses a flag bit on the final hash object in the regular splat, as opposed to a previous approach that used a VM frame flag. The hash flag approach is less invasive, and handles some cases that the VM frame flag approach does not, such as saving the argument splat array and splatting it later: ruby2_keywords def foo(*args) @args = args bar end def bar baz(*@args) end def baz(*args, **kw) [args, kw] end foo(a:1) #=> [[], {a: 1}] foo({a: 1}, **{}) #=> [[{a: 1}], {}] foo({a: 1}) #=> 2.7: [[], {a: 1}] # and warning foo({a: 1}) #=> 3.0: [[{a: 1}], {}] It doesn't handle some cases that the VM frame flag handles, such as when the final hash object is replaced using Hash#merge, but those cases are probably less common and are unlikely to properly support keyword argument separation. Use ruby2_keywords to handle argument delegation in the delegate library.
-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);