summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeremy Evans <code@jeremyevans.net>2021-11-19 09:38:22 -0800
committerJeremy Evans <code@jeremyevans.net>2021-12-30 14:37:42 -0800
commitf53dfab95c30e222f67e610234f63d3e9189234d (patch)
tree83a9c1d49e3f96e36f75721d0ca1437aa2b56677
parent2d2ee338f3427d39d9977c77b09e5d335b6e362b (diff)
downloadruby-f53dfab95c30e222f67e610234f63d3e9189234d.tar.gz
Add support for anonymous rest and keyword rest argument forwarding
This allows for the following syntax: ```ruby def foo(*) bar(*) end def baz(**) quux(**) end ``` This is a natural addition after the introduction of anonymous block forwarding. Anonymous rest and keyword rest arguments were already supported in method parameters, this just allows them to be used as arguments to other methods. The same advantages of anonymous block forwarding apply to rest and keyword rest argument forwarding. This has some minor changes to #parameters output. Now, instead of `[:rest], [:keyrest]`, you get `[:rest, :*], [:keyrest, :**]`. These were already used for `...` forwarding, so I think it makes it more consistent to include them in other cases. If we want to use `[:rest], [:keyrest]` in both cases, that is also possible. I don't think the previous behavior of `[:rest], [:keyrest]` in the non-... case and `[:rest, :*], [:keyrest, :**]` in the ... case makes sense, but if we did want that behavior, we'll have to make more substantial changes, such as using a different ID in the ... forwarding case. Implements [Feature #18351]
-rw-r--r--NEWS.md15
-rw-r--r--doc/syntax/methods.rdoc14
-rw-r--r--parse.y39
-rw-r--r--proc.c16
-rw-r--r--spec/ruby/core/method/parameters_spec.rb15
-rw-r--r--spec/ruby/core/proc/parameters_spec.rb12
-rw-r--r--test/ruby/test_iseq.rb14
-rw-r--r--test/ruby/test_method.rb12
-rw-r--r--test/ruby/test_proc.rb2
-rw-r--r--test/ruby/test_syntax.rb28
10 files changed, 146 insertions, 21 deletions
diff --git a/NEWS.md b/NEWS.md
index 69f57d5997..06696129a5 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -7,6 +7,19 @@ Note that each entry is kept to a minimum, see links for details.
## Language changes
+* Anonymous rest and keyword rest arguments can now be passed as
+ arguments, instead of just used in method parameters.
+ [[Feature #18351]]
+
+ ```ruby
+ def foo(*)
+ bar(*)
+ end
+ def baz(**)
+ quux(**)
+ end
+ ```
+
## Command line options
## Core classes updates
@@ -52,3 +65,5 @@ Note: Excluding feature bug fixes.
## IRB Autocomplete and Document Display
## Miscellaneous changes
+
+[Feature #18351]: https://bugs.ruby-lang.org/issues/18351
diff --git a/doc/syntax/methods.rdoc b/doc/syntax/methods.rdoc
index 2bb350def1..8dafa6bb0c 100644
--- a/doc/syntax/methods.rdoc
+++ b/doc/syntax/methods.rdoc
@@ -441,6 +441,13 @@ Also, note that a bare <code>*</code> can be used to ignore arguments:
def ignore_arguments(*)
end
+You can also use a bare <code>*</code> when calling a method to pass the
+arguments directly to another method:
+
+ def delegate_arguments(*)
+ other_method(*)
+ end
+
=== Keyword Arguments
Keyword arguments are similar to positional arguments with default values:
@@ -481,6 +488,13 @@ Also, note that <code>**</code> can be used to ignore keyword arguments:
def ignore_keywords(**)
end
+You can also use <code>**</code> when calling a method to delegate
+keyword arguments to another method:
+
+ def delegate_keywords(**)
+ other_method(**)
+ end
+
To mark a method as accepting keywords, but not actually accepting
keywords, you can use the <code>**nil</code>:
diff --git a/parse.y b/parse.y
index 7555d0db16..794c818e34 100644
--- a/parse.y
+++ b/parse.y
@@ -427,6 +427,8 @@ static void token_info_drop(struct parser_params *p, const char *token, rb_code_
#define lambda_beginning_p() (p->lex.lpar_beg == p->lex.paren_nest)
#define ANON_BLOCK_ID '&'
+#define ANON_REST_ID '*'
+#define ANON_KEYWORD_REST_ID idPow
static enum yytokentype yylex(YYSTYPE*, YYLTYPE*, struct parser_params*);
@@ -2890,6 +2892,16 @@ args : arg_value
/*% %*/
/*% ripper: args_add_star!(args_new!, $2) %*/
}
+ | tSTAR
+ {
+ /*%%%*/
+ if (!local_id(p, ANON_REST_ID)) {
+ compile_error(p, "no anonymous rest parameter");
+ }
+ $$ = NEW_SPLAT(NEW_LVAR(ANON_REST_ID, &@1), &@$);
+ /*% %*/
+ /*% ripper: args_add_star!(args_new!, Qnil) %*/
+ }
| args ',' arg_value
{
/*%%%*/
@@ -2904,6 +2916,16 @@ args : arg_value
/*% %*/
/*% ripper: args_add_star!($1, $4) %*/
}
+ | args ',' tSTAR
+ {
+ /*%%%*/
+ if (!local_id(p, ANON_REST_ID)) {
+ compile_error(p, "no anonymous rest parameter");
+ }
+ $$ = rest_arg_append(p, $1, NEW_LVAR(ANON_REST_ID, &@3), &@$);
+ /*% %*/
+ /*% ripper: args_add_star!($1, Qnil) %*/
+ }
;
/* value */
@@ -5479,8 +5501,7 @@ f_kwrest : kwrest_mark tIDENTIFIER
| kwrest_mark
{
/*%%%*/
- $$ = internal_id(p);
- arg_var(p, $$);
+ arg_var(p, shadowing_lvar(p, get_id(ANON_KEYWORD_REST_ID)));
/*% %*/
/*% ripper: kwrest_param!(Qnil) %*/
}
@@ -5555,8 +5576,7 @@ f_rest_arg : restarg_mark tIDENTIFIER
| restarg_mark
{
/*%%%*/
- $$ = internal_id(p);
- arg_var(p, $$);
+ arg_var(p, shadowing_lvar(p, get_id(ANON_REST_ID)));
/*% %*/
/*% ripper: rest_param!(Qnil) %*/
}
@@ -5710,6 +5730,17 @@ assoc : arg_value tASSOC arg_value
/*% %*/
/*% ripper: assoc_splat!($2) %*/
}
+ | tDSTAR
+ {
+ /*%%%*/
+ if (!local_id(p, ANON_KEYWORD_REST_ID)) {
+ compile_error(p, "no anonymous keyword rest parameter");
+ }
+ $$ = list_append(p, NEW_LIST(0, &@$),
+ NEW_LVAR(ANON_KEYWORD_REST_ID, &@$));
+ /*% %*/
+ /*% ripper: assoc_splat!(Qnil) %*/
+ }
;
operation : tIDENTIFIER
diff --git a/proc.c b/proc.c
index d075b7382e..6cdd7bd836 100644
--- a/proc.c
+++ b/proc.c
@@ -3124,6 +3124,16 @@ method_inspect(VALUE method)
rb_str_buf_cat2(str, "(");
+ if (RARRAY_LEN(params) == 3 &&
+ RARRAY_AREF(RARRAY_AREF(params, 0), 0) == rest &&
+ RARRAY_AREF(RARRAY_AREF(params, 0), 1) == ID2SYM('*') &&
+ RARRAY_AREF(RARRAY_AREF(params, 1), 0) == keyrest &&
+ RARRAY_AREF(RARRAY_AREF(params, 1), 1) == ID2SYM(idPow) &&
+ RARRAY_AREF(RARRAY_AREF(params, 2), 0) == block &&
+ RARRAY_AREF(RARRAY_AREF(params, 2), 1) == ID2SYM('&')) {
+ forwarding = 1;
+ }
+
for (int i = 0; i < RARRAY_LEN(params); i++) {
pair = RARRAY_AREF(params, i);
kind = RARRAY_AREF(pair, 0);
@@ -3159,8 +3169,7 @@ method_inspect(VALUE method)
}
else if (kind == rest) {
if (name == ID2SYM('*')) {
- forwarding = 1;
- rb_str_cat_cstr(str, "...");
+ rb_str_cat_cstr(str, forwarding ? "..." : "*");
}
else {
rb_str_catf(str, "*%"PRIsVALUE, name);
@@ -3173,6 +3182,9 @@ method_inspect(VALUE method)
else if (i > 0) {
rb_str_set_len(str, RSTRING_LEN(str) - 2);
}
+ else {
+ rb_str_cat_cstr(str, "**");
+ }
}
else if (kind == block) {
if (name == ID2SYM('&')) {
diff --git a/spec/ruby/core/method/parameters_spec.rb b/spec/ruby/core/method/parameters_spec.rb
index 3fdaf9ce6f..67b42afafa 100644
--- a/spec/ruby/core/method/parameters_spec.rb
+++ b/spec/ruby/core/method/parameters_spec.rb
@@ -222,9 +222,18 @@ describe "Method#parameters" do
m.method(:handled_via_method_missing).parameters.should == [[:rest]]
end
- it "adds nameless rest arg for \"star\" argument" do
- m = MethodSpecs::Methods.new
- m.method(:one_unnamed_splat).parameters.should == [[:rest]]
+ ruby_version_is '3.1' do
+ it "adds * rest arg for \"star\" argument" do
+ m = MethodSpecs::Methods.new
+ m.method(:one_unnamed_splat).parameters.should == [[:rest, :*]]
+ end
+ end
+
+ ruby_version_is ''...'3.1' do
+ it "adds nameless rest arg for \"star\" argument" do
+ m = MethodSpecs::Methods.new
+ m.method(:one_unnamed_splat).parameters.should == [[:rest]]
+ end
end
it "returns the args and block for a splat and block argument" do
diff --git a/spec/ruby/core/proc/parameters_spec.rb b/spec/ruby/core/proc/parameters_spec.rb
index 5fb5cf418d..60620722f8 100644
--- a/spec/ruby/core/proc/parameters_spec.rb
+++ b/spec/ruby/core/proc/parameters_spec.rb
@@ -80,8 +80,16 @@ describe "Proc#parameters" do
-> x {}.parameters.should == [[:req, :x]]
end
- it "adds nameless rest arg for \"star\" argument" do
- -> x, * {}.parameters.should == [[:req, :x], [:rest]]
+ ruby_version_is '3.1' do
+ it "adds * rest arg for \"star\" argument" do
+ -> x, * {}.parameters.should == [[:req, :x], [:rest, :*]]
+ end
+ end
+
+ ruby_version_is ''...'3.1' do
+ it "adds nameless rest arg for \"star\" argument" do
+ -> x, * {}.parameters.should == [[:req, :x], [:rest]]
+ end
end
it "does not add locals as block options with a block and splat" do
diff --git a/test/ruby/test_iseq.rb b/test/ruby/test_iseq.rb
index f01d36cc5a..1ee41e6e81 100644
--- a/test/ruby/test_iseq.rb
+++ b/test/ruby/test_iseq.rb
@@ -162,7 +162,7 @@ class TestISeq < Test::Unit::TestCase
end
obj = Object.new
def obj.foo(*) nil.instance_eval{ ->{super} } end
- assert_raise_with_message(Ractor::IsolationError, /hidden variable/) do
+ assert_raise_with_message(Ractor::IsolationError, /refer unshareable object \[\] from variable `\*'/) do
Ractor.make_shareable(obj.foo)
end
end
@@ -392,10 +392,18 @@ class TestISeq < Test::Unit::TestCase
def anon_star(*); end
- def test_anon_param_in_disasm
+ def test_anon_rest_param_in_disasm
iseq = RubyVM::InstructionSequence.of(method(:anon_star))
param_names = iseq.to_a[iseq.to_a.index(:method) + 1]
- assert_equal [2], param_names
+ assert_equal [:*], param_names
+ end
+
+ def anon_keyrest(**); end
+
+ def test_anon_keyrest_param_in_disasm
+ iseq = RubyVM::InstructionSequence.of(method(:anon_keyrest))
+ param_names = iseq.to_a[iseq.to_a.index(:method) + 1]
+ assert_equal [:**], param_names
end
def anon_block(&); end
diff --git a/test/ruby/test_method.rb b/test/ruby/test_method.rb
index da68787933..d6de330dff 100644
--- a/test/ruby/test_method.rb
+++ b/test/ruby/test_method.rb
@@ -566,9 +566,9 @@ class TestMethod < Test::Unit::TestCase
assert_equal([[:req, :a], [:rest, :b], [:req, :c]], method(:mo5).parameters)
assert_equal([[:req, :a], [:rest, :b], [:req, :c], [:block, :d]], method(:mo6).parameters)
assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:block, :e]], method(:mo7).parameters)
- assert_equal([[:req, :a], [:opt, :b], [:rest], [:req, :d], [:block, :e]], method(:mo8).parameters)
+ assert_equal([[:req, :a], [:opt, :b], [:rest, :*], [:req, :d], [:block, :e]], method(:mo8).parameters)
assert_equal([[:req], [:block, :b]], method(:ma1).parameters)
- assert_equal([[:keyrest]], method(:mk1).parameters)
+ assert_equal([[:keyrest, :**]], method(:mk1).parameters)
assert_equal([[:keyrest, :o]], method(:mk2).parameters)
assert_equal([[:req, :a], [:keyrest, :o]], method(:mk3).parameters)
assert_equal([[:opt, :a], [:keyrest, :o]], method(:mk4).parameters)
@@ -592,9 +592,9 @@ class TestMethod < Test::Unit::TestCase
assert_equal([[:req, :a], [:rest, :b], [:req, :c]], self.class.instance_method(:mo5).parameters)
assert_equal([[:req, :a], [:rest, :b], [:req, :c], [:block, :d]], self.class.instance_method(:mo6).parameters)
assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:block, :e]], self.class.instance_method(:mo7).parameters)
- assert_equal([[:req, :a], [:opt, :b], [:rest], [:req, :d], [:block, :e]], self.class.instance_method(:mo8).parameters)
+ assert_equal([[:req, :a], [:opt, :b], [:rest, :*], [:req, :d], [:block, :e]], self.class.instance_method(:mo8).parameters)
assert_equal([[:req], [:block, :b]], self.class.instance_method(:ma1).parameters)
- assert_equal([[:keyrest]], self.class.instance_method(:mk1).parameters)
+ assert_equal([[:keyrest, :**]], self.class.instance_method(:mk1).parameters)
assert_equal([[:keyrest, :o]], self.class.instance_method(:mk2).parameters)
assert_equal([[:req, :a], [:keyrest, :o]], self.class.instance_method(:mk3).parameters)
assert_equal([[:opt, :a], [:keyrest, :o]], self.class.instance_method(:mk4).parameters)
@@ -619,7 +619,7 @@ class TestMethod < Test::Unit::TestCase
assert_equal([[:req, :a], [:rest, :b], [:req, :c], [:block, :d]], method(:pmo6).parameters)
assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:block, :e]], method(:pmo7).parameters)
assert_equal([[:req], [:block, :b]], method(:pma1).parameters)
- assert_equal([[:keyrest]], method(:pmk1).parameters)
+ assert_equal([[:keyrest, :**]], method(:pmk1).parameters)
assert_equal([[:keyrest, :o]], method(:pmk2).parameters)
assert_equal([[:req, :a], [:keyrest, :o]], method(:pmk3).parameters)
assert_equal([[:opt, :a], [:keyrest, :o]], method(:pmk4).parameters)
@@ -643,7 +643,7 @@ class TestMethod < Test::Unit::TestCase
assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:block, :e]], self.class.instance_method(:pmo7).parameters)
assert_equal([[:req], [:block, :b]], self.class.instance_method(:pma1).parameters)
assert_equal([[:req], [:block, :b]], self.class.instance_method(:pma1).parameters)
- assert_equal([[:keyrest]], self.class.instance_method(:pmk1).parameters)
+ assert_equal([[:keyrest, :**]], self.class.instance_method(:pmk1).parameters)
assert_equal([[:keyrest, :o]], self.class.instance_method(:pmk2).parameters)
assert_equal([[:req, :a], [:keyrest, :o]], self.class.instance_method(:pmk3).parameters)
assert_equal([[:opt, :a], [:keyrest, :o]], self.class.instance_method(:pmk4).parameters)
diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb
index 51872e49be..05b2493b24 100644
--- a/test/ruby/test_proc.rb
+++ b/test/ruby/test_proc.rb
@@ -1261,7 +1261,7 @@ class TestProc < Test::Unit::TestCase
assert_equal([[:req, :a], [:rest, :b], [:req, :c], [:block, :d]], method(:pmo6).to_proc.parameters)
assert_equal([[:req, :a], [:opt, :b], [:rest, :c], [:req, :d], [:block, :e]], method(:pmo7).to_proc.parameters)
assert_equal([[:req], [:block, :b]], method(:pma1).to_proc.parameters)
- assert_equal([[:keyrest]], method(:pmk1).to_proc.parameters)
+ assert_equal([[:keyrest, :**]], method(:pmk1).to_proc.parameters)
assert_equal([[:keyrest, :o]], method(:pmk2).to_proc.parameters)
assert_equal([[:req, :a], [:keyrest, :o]], method(:pmk3).to_proc.parameters)
assert_equal([[:opt, :a], [:keyrest, :o]], method(:pmk4).to_proc.parameters)
diff --git a/test/ruby/test_syntax.rb b/test/ruby/test_syntax.rb
index b71f492f9c..622527dafe 100644
--- a/test/ruby/test_syntax.rb
+++ b/test/ruby/test_syntax.rb
@@ -78,6 +78,34 @@ class TestSyntax < Test::Unit::TestCase
end;
end
+ def test_anonymous_rest_forwarding
+ assert_syntax_error("def b; c(*); end", /no anonymous rest parameter/)
+ assert_syntax_error("def b; c(1, *); end", /no anonymous rest parameter/)
+ assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}")
+ begin;
+ def b(*); c(*) end
+ def c(*a); a end
+ def d(*); b(*, *) end
+ assert_equal([1, 2], b(1, 2))
+ assert_equal([1, 2, 1, 2], d(1, 2))
+ end;
+ end
+
+ def test_anonymous_keyword_rest_forwarding
+ assert_syntax_error("def b; c(**); end", /no anonymous keyword rest parameter/)
+ assert_syntax_error("def b; c(k: 1, **); end", /no anonymous keyword rest parameter/)
+ assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}")
+ begin;
+ def b(**); c(**) end
+ def c(**kw); kw end
+ def d(**); b(k: 1, **) end
+ def e(**); b(**, k: 1) end
+ assert_equal({a: 1, k: 3}, b(a: 1, k: 3))
+ assert_equal({a: 1, k: 3}, d(a: 1, k: 3))
+ assert_equal({a: 1, k: 1}, e(a: 1, k: 3))
+ end;
+ end
+
def test_newline_in_block_parameters
bug = '[ruby-dev:45292]'
["", "a", "a, b"].product(["", ";x", [";", "x"]]) do |params|