diff options
author | Jeremy Evans <[email protected]> | 2021-11-19 09:38:22 -0800 |
---|---|---|
committer | Jeremy Evans <[email protected]> | 2021-12-30 14:37:42 -0800 |
commit | f53dfab95c30e222f67e610234f63d3e9189234d (patch) | |
tree | 83a9c1d49e3f96e36f75721d0ca1437aa2b56677 | |
parent | 2d2ee338f3427d39d9977c77b09e5d335b6e362b (diff) |
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]
Notes
Notes:
Merged: https://github.com/ruby/ruby/pull/5148
-rw-r--r-- | NEWS.md | 15 | ||||
-rw-r--r-- | doc/syntax/methods.rdoc | 14 | ||||
-rw-r--r-- | parse.y | 39 | ||||
-rw-r--r-- | proc.c | 16 | ||||
-rw-r--r-- | spec/ruby/core/method/parameters_spec.rb | 15 | ||||
-rw-r--r-- | spec/ruby/core/proc/parameters_spec.rb | 12 | ||||
-rw-r--r-- | test/ruby/test_iseq.rb | 14 | ||||
-rw-r--r-- | test/ruby/test_method.rb | 12 | ||||
-rw-r--r-- | test/ruby/test_proc.rb | 2 | ||||
-rw-r--r-- | test/ruby/test_syntax.rb | 28 |
10 files changed, 146 insertions, 21 deletions
@@ -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>: @@ -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 @@ -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| |