From 4adb012926f8bd6011168327d8832cf19976de40 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Thu, 18 Nov 2021 12:44:19 -0800 Subject: [PATCH] Anonymous block forwarding allows a method to forward a passed block to another method without having to provide a name for the block parameter. Implements [Feature #11256] Co-authored-by: Yusuke Endoh mame@ruby-lang.org Co-authored-by: Nobuyoshi Nakada nobu@ruby-lang.org --- NEWS.md | 10 ++++++++++ doc/syntax/methods.rdoc | 9 ++++++++- parse.y | 21 +++++++++++++++++++++ test/ruby/test_iseq.rb | 18 ++++++++++++++++++ test/ruby/test_parse.rb | 2 +- test/ruby/test_syntax.rb | 12 ++++++++++++ 6 files changed, 70 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 2e4cbc6742..3e8035714b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -7,6 +7,15 @@ Note that each entry is kept to a minimum, see links for details. ## Language changes +* The block arguments can be now be anonymous, if the block will + only be passed to another method. [[Feature #11256]] + + ```ruby + def foo(&) + bar(&) + end + ``` + * Pin operator now takes an expression. [[Feature #17411]] ```ruby @@ -412,6 +421,7 @@ See [the repository](https://github.com/ruby/error_highlight) in detail. [Bug #4443]: https://bugs.ruby-lang.org/issues/4443 [Feature #6210]: https://bugs.ruby-lang.org/issues/6210 +[Feature #11256]: https://bugs.ruby-lang.org/issues/11256 [Feature #12194]: https://bugs.ruby-lang.org/issues/12194 [Feature #12495]: https://bugs.ruby-lang.org/issues/12495 [Feature #14256]: https://bugs.ruby-lang.org/issues/14256 diff --git a/doc/syntax/methods.rdoc b/doc/syntax/methods.rdoc index e86cc2c00c..2bb350def1 100644 --- a/doc/syntax/methods.rdoc +++ b/doc/syntax/methods.rdoc @@ -515,8 +515,15 @@ Most frequently the block argument is used to pass a block to another method: @items.each(&block) end +You are not required to give a name to the block if you will just be passing +it to another method: + + def each_item(&) + @items.each(&) + end + If you are only going to call the block and will not otherwise manipulate it -or send it to another method using yield without an explicit +or send it to another method, using yield without an explicit block parameter is preferred. This method is equivalent to the first method in this section: diff --git a/parse.y b/parse.y index 8bfb7f2757..b6498651bf 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 '&' + static enum yytokentype yylex(YYSTYPE*, YYLTYPE*, struct parser_params*); #ifndef RIPPER @@ -2846,6 +2848,17 @@ block_arg : tAMPER arg_value /*% %*/ /*% ripper: $2 %*/ } + | tAMPER + { + /*%%%*/ + if (!local_id(p, ANON_BLOCK_ID)) { + compile_error(p, "no anonymous block parameter"); + } + $$ = NEW_BLOCK_PASS(NEW_LVAR(ANON_BLOCK_ID, &@1), &@$); + /*% + $$ = Qnil; + %*/ + } ; opt_block_arg : ',' block_arg @@ -5541,6 +5554,14 @@ f_block_arg : blkarg_mark tIDENTIFIER /*% %*/ /*% ripper: blockarg!($2) %*/ } + | blkarg_mark + { + /*%%%*/ + arg_var(p, shadowing_lvar(p, get_id(ANON_BLOCK_ID))); + /*% + $$ = dispatch1(blockarg, Qnil); + %*/ + } ; opt_f_block_arg : ',' f_block_arg diff --git a/test/ruby/test_iseq.rb b/test/ruby/test_iseq.rb index 692549efa0..49f12019dc 100644 --- a/test/ruby/test_iseq.rb +++ b/test/ruby/test_iseq.rb @@ -125,6 +125,16 @@ class TestISeq < Test::Unit::TestCase assert_equal(42, ISeq.load_from_binary(iseq.to_binary).eval) end + def test_super_with_anonymous_block + iseq = compile(<<~EOF) + def touch3(&block) # :nodoc: + foo { super } + end + 42 + EOF + assert_equal(42, ISeq.load_from_binary(iseq.to_binary).eval) + end + def test_ractor_unshareable_outer_variable name = "\u{2603 26a1}" y = eval("proc {#{name} = nil; proc {|x| #{name} = x}}").call @@ -373,6 +383,14 @@ class TestISeq < Test::Unit::TestCase assert_equal [2], param_names end + def anon_block(&); end + + def test_anon_block_param_in_disasm + iseq = RubyVM::InstructionSequence.of(method(:anon_block)) + param_names = iseq.to_a[iseq.to_a.index(:method) + 1] + assert_equal [:&], param_names + end + def strip_lineno(source) source.gsub(/^.*?: /, "") end diff --git a/test/ruby/test_parse.rb b/test/ruby/test_parse.rb index 3120016e60..d697a29c1c 100644 --- a/test/ruby/test_parse.rb +++ b/test/ruby/test_parse.rb @@ -1044,7 +1044,7 @@ x = __ENCODING__ end; assert_syntax_error("def\nf(000)end", /^ \^~~/) - assert_syntax_error("def\nf(&)end", /^ \^/) + assert_syntax_error("def\nf(&0)end", /^ \^/) end def test_method_location_in_rescue diff --git a/test/ruby/test_syntax.rb b/test/ruby/test_syntax.rb index 11953ab563..ce1e489992 100644 --- a/test/ruby/test_syntax.rb +++ b/test/ruby/test_syntax.rb @@ -66,6 +66,18 @@ class TestSyntax < Test::Unit::TestCase f&.close! end + def test_anonymous_block_forwarding + assert_syntax_error("def b; c(&); end", /no anonymous block parameter/) + assert_separately([], "#{<<-"begin;"}\n#{<<-'end;'}") + begin; + def b(&); c(&) end + def c(&); yield 1 end + a = nil + b{|c| a = c} + assert_equal(1, a) + end; + end + def test_newline_in_block_parameters bug = '[ruby-dev:45292]' ["", "a", "a, b"].product(["", ";x", [";", "x"]]) do |params|