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]
This commit is contained in:
Jeremy Evans 2021-11-19 09:38:22 -08:00
Родитель 2d2ee338f3
Коммит f53dfab95c
10 изменённых файлов: 146 добавлений и 21 удалений

15
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

Просмотреть файл

@ -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>:

39
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

16
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('&')) {

Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -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)

Просмотреть файл

@ -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)

Просмотреть файл

@ -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|