diff options
| author | Nobuyoshi Nakada <nobu@ruby-lang.org> | 2024-02-20 19:34:50 +0900 |
|---|---|---|
| committer | Kevin Newton <kddnewton@gmail.com> | 2026-02-15 14:12:15 -0500 |
| commit | 116d402067c1d01ed67b16ec93fb523bd5109d07 (patch) | |
| tree | 9ed0f81052e64934218586ae691dd4d2d1fbec8d | |
| parent | 2065b55980a0fc6386d58e4ede37d60c50e5b62f (diff) | |
[Feature #19979] Method definition with `&nil`
Allow methods to declare that they don't accept a block via `&nil`.
| -rw-r--r-- | compile.c | 11 | ||||
| -rw-r--r-- | parse.y | 9 | ||||
| -rw-r--r-- | rubyparser.h | 1 | ||||
| -rw-r--r-- | spec/ruby/language/block_spec.rb | 11 | ||||
| -rw-r--r-- | spec/ruby/language/method_spec.rb | 12 | ||||
| -rw-r--r-- | test/ruby/test_syntax.rb | 43 | ||||
| -rw-r--r-- | vm_args.c | 23 | ||||
| -rw-r--r-- | vm_core.h | 1 | ||||
| -rw-r--r-- | vm_insnhelper.c | 9 | ||||
| -rw-r--r-- | zjit/src/cruby_bindings.inc.rs | 38 |
10 files changed, 149 insertions, 9 deletions
@@ -2096,7 +2096,7 @@ iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const optargs, const NODE *cons if (node_args) { struct rb_iseq_constant_body *const body = ISEQ_BODY(iseq); - struct rb_args_info *args = &RNODE_ARGS(node_args)->nd_ainfo; + const struct rb_args_info *const args = &RNODE_ARGS(node_args)->nd_ainfo; ID rest_id = 0; int last_comma = 0; ID block_id = 0; @@ -2193,7 +2193,10 @@ iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const optargs, const NODE *cons body->param.flags.accepts_no_kwarg = TRUE; } - if (block_id) { + if (args->no_blockarg) { + body->param.flags.accepts_no_block = TRUE; + } + else if (block_id) { body->param.block_start = arg_size++; body->param.flags.has_block = TRUE; iseq_set_use_block(iseq); @@ -13678,7 +13681,8 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq) (body->param.flags.anon_rest << 10) | (body->param.flags.anon_kwrest << 11) | (body->param.flags.use_block << 12) | - (body->param.flags.forwardable << 13) ; + (body->param.flags.forwardable << 13) | + (body->param.flags.accepts_no_block << 14); #if IBF_ISEQ_ENABLE_LOCAL_BUFFER # define IBF_BODY_OFFSET(x) (x) @@ -13898,6 +13902,7 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset) load_body->param.flags.anon_kwrest = (param_flags >> 11) & 1; load_body->param.flags.use_block = (param_flags >> 12) & 1; load_body->param.flags.forwardable = (param_flags >> 13) & 1; + load_body->param.flags.accepts_no_block = (param_flags >> 14) & 1; load_body->param.size = param_size; load_body->param.lead_num = param_lead_num; load_body->param.opt_num = param_opt_num; @@ -6562,6 +6562,11 @@ f_block_arg : blkarg_mark tIDENTIFIER $$ = $2; /*% ripper: blockarg!($:2) %*/ } + | blkarg_mark keyword_nil + { + $$ = idNil; + /*% ripper: blockarg!(ID2VAL(idNil)) %*/ + } | blkarg_mark { arg_var(p, idFWD_BLOCK); @@ -14474,6 +14479,10 @@ new_args_tail(struct parser_params *p, rb_node_kw_arg_t *kw_args, ID kw_rest_arg struct rb_args_info *args = &node->nd_ainfo; if (p->error_p) return node; + if (block == idNil) { + block = 0; + args->no_blockarg = TRUE; + } args->block_arg = block; args->kw_args = kw_args; diff --git a/rubyparser.h b/rubyparser.h index 36a2dc30a6..2ed93e9894 100644 --- a/rubyparser.h +++ b/rubyparser.h @@ -764,6 +764,7 @@ struct rb_args_info { struct RNode_OPT_ARG *opt_args; unsigned int no_kwarg: 1; + unsigned int no_blockarg: 1; unsigned int forwarding: 1; }; diff --git a/spec/ruby/language/block_spec.rb b/spec/ruby/language/block_spec.rb index 67aad76c57..1890f0726a 100644 --- a/spec/ruby/language/block_spec.rb +++ b/spec/ruby/language/block_spec.rb @@ -1110,6 +1110,17 @@ describe "`it` calls without arguments in a block" do end end end + + ruby_version_is "3.4" do + it "works alongside disallowed block argument" do + no_block = eval <<-EOF + proc {|arg1, &nil| arg1} + EOF + + no_block.call(:a).should == :a + -> { no_block.call(:a) {} }.should raise_error(ArgumentError, 'no block accepted') + end + end end # Duplicates specs in language/it_parameter_spec.rb diff --git a/spec/ruby/language/method_spec.rb b/spec/ruby/language/method_spec.rb index 8f9f094fd8..d12fd71b21 100644 --- a/spec/ruby/language/method_spec.rb +++ b/spec/ruby/language/method_spec.rb @@ -1127,6 +1127,18 @@ describe "A method" do result = m(1, {foo: :bar}) result.should == [1, nil, nil, {foo: :bar}, nil, {}] end + + ruby_version_is "3.4" do + evaluate <<-ruby do + def m(a, &nil); a end; + ruby + + m(1).should == 1 + + -> { m(1) {} }.should raise_error(ArgumentError, 'no block accepted') + -> { m(1, &proc {}) }.should raise_error(ArgumentError, 'no block accepted') + end + end end context 'when passing an empty keyword splat to a method that does not accept keywords' do diff --git a/test/ruby/test_syntax.rb b/test/ruby/test_syntax.rb index e868967e8b..70e1956816 100644 --- a/test/ruby/test_syntax.rb +++ b/test/ruby/test_syntax.rb @@ -202,6 +202,49 @@ class TestSyntax < Test::Unit::TestCase assert_syntax_error("def f(...); g(&); end", /no anonymous block parameter/) end + def test_no_block_argument_in_method + assert_valid_syntax("def f(&nil) end") + assert_valid_syntax("def f(a, &nil) end") + assert_valid_syntax("def f(*rest, &nil) end") + assert_valid_syntax("def f(*rest, p, &nil) end") + assert_valid_syntax("def f(a, *rest, &nil) end") + assert_valid_syntax("def f(a, *rest, p, &nil) end") + assert_valid_syntax("def f(a, k: nil, &nil) end") + assert_valid_syntax("def f(a, k: nil, **kw, &nil) end") + assert_valid_syntax("def f(a, *rest, k: nil, &nil) end") + assert_valid_syntax("def f(a, *rest, k: nil, **kw, &nil) end") + assert_valid_syntax("def f(a, *rest, p, k: nil, &nil) end") + assert_valid_syntax("def f(a, *rest, p, k: nil, **kw, &nil) end") + + obj = Object.new + obj.instance_eval "def f(&nil) end" + assert_raise_with_message(ArgumentError, /block accepted/) {obj.f {}} + assert_raise_with_message(ArgumentError, /block accepted/) {obj.f(&proc {})} + end + + def test_no_block_argument_in_block + assert_valid_syntax("proc do |&nil| end") + assert_valid_syntax("proc do |a, &nil| end") + assert_valid_syntax("proc do |*rest, &nil| end") + assert_valid_syntax("proc do |*rest, p, &nil| end") + assert_valid_syntax("proc do |a, *rest, &nil| end") + assert_valid_syntax("proc do |a, *rest, p, &nil| end") + assert_valid_syntax("proc do |a, k: nil, &nil| end") + assert_valid_syntax("proc do |a, k: nil, **kw, &nil| end") + assert_valid_syntax("proc do |a, *rest, k: nil, &nil| end") + assert_valid_syntax("proc do |a, *rest, k: nil, **kw, &nil| end") + assert_valid_syntax("proc do |a, *rest, p, k: nil, &nil| end") + assert_valid_syntax("proc do |a, *rest, p, k: nil, **kw, &nil| end") + + pr = eval "proc {|&nil|}" + assert_nil(pr.call) + assert_raise_with_message(ArgumentError, /block accepted/) {pr.call {}} + pr = eval "proc {|a, &nil| a}" + assert_nil(pr.call) + assert_equal(1, pr.call(1)) + assert_raise_with_message(ArgumentError, /block accepted/) {pr.call {}} + end + def test_newline_in_block_parameters bug = '[ruby-dev:45292]' ["", "a", "a, b"].product(["", ";x", [";", "x"]]) do |params| @@ -12,6 +12,8 @@ NORETURN(static void raise_argument_error(rb_execution_context_t *ec, const rb_i NORETURN(static void argument_arity_error(rb_execution_context_t *ec, const rb_iseq_t *iseq, const rb_callable_method_entry_t *cme, const int miss_argc, const int min_argc, const int max_argc)); NORETURN(static void argument_kw_error(rb_execution_context_t *ec, const rb_iseq_t *iseq, const rb_callable_method_entry_t *cme, const char *error, const VALUE keys)); VALUE rb_keyword_error_new(const char *error, VALUE keys); /* class.c */ +static VALUE set_error_backtrace(rb_execution_context_t *ec, const rb_iseq_t *iseq, const rb_callable_method_entry_t *cme, const VALUE exc); + static VALUE method_missing(rb_execution_context_t *ec, VALUE obj, ID id, int argc, const VALUE *argv, enum method_missing_reason call_status, int kw_splat); const rb_callable_method_entry_t *rb_resolve_refined_method_callable(VALUE refinements, const rb_callable_method_entry_t *me); @@ -959,7 +961,15 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co argument_kw_error(ec, iseq, cme, "unknown", rb_hash_keys(keyword_hash)); } - if (ISEQ_BODY(iseq)->param.flags.has_block) { + if (ISEQ_BODY(iseq)->param.flags.accepts_no_block) { + VALUE given_block; + args_setup_block_parameter(ec, calling, &given_block); + if (!NIL_P(given_block)) { + VALUE exc = rb_exc_new_cstr(rb_eArgError, "no block accepted"); + rb_exc_raise(set_error_backtrace(ec, iseq, cme, exc)); + } + } + else if (ISEQ_BODY(iseq)->param.flags.has_block) { if (ISEQ_BODY(iseq)->local_iseq == iseq) { /* Do nothing */ } @@ -981,8 +991,8 @@ setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * co return opt_pc; } -static void -raise_argument_error(rb_execution_context_t *ec, const rb_iseq_t *iseq, const rb_callable_method_entry_t *cme, const VALUE exc) +static VALUE +set_error_backtrace(rb_execution_context_t *ec, const rb_iseq_t *iseq, const rb_callable_method_entry_t *cme, const VALUE exc) { VALUE at; @@ -1000,6 +1010,13 @@ raise_argument_error(rb_execution_context_t *ec, const rb_iseq_t *iseq, const rb rb_ivar_set(exc, idBt_locations, at); rb_exc_set_backtrace(exc, at); + return exc; +} + +static void +raise_argument_error(rb_execution_context_t *ec, const rb_iseq_t *iseq, const rb_callable_method_entry_t *cme, const VALUE exc) +{ + set_error_backtrace(ec, iseq, cme, exc); rb_exc_raise(exc); } @@ -456,6 +456,7 @@ struct rb_iseq_constant_body { unsigned int anon_kwrest: 1; unsigned int use_block: 1; unsigned int forwardable: 1; + unsigned int accepts_no_block: 1; } flags; unsigned int size; diff --git a/vm_insnhelper.c b/vm_insnhelper.c index a27bf5f49b..95422caaa6 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -2732,7 +2732,8 @@ rb_simple_iseq_p(const rb_iseq_t *iseq) ISEQ_BODY(iseq)->param.flags.has_kwrest == FALSE && ISEQ_BODY(iseq)->param.flags.accepts_no_kwarg == FALSE && ISEQ_BODY(iseq)->param.flags.forwardable == FALSE && - ISEQ_BODY(iseq)->param.flags.has_block == FALSE; + ISEQ_BODY(iseq)->param.flags.has_block == FALSE && + ISEQ_BODY(iseq)->param.flags.accepts_no_block == FALSE; } bool @@ -2745,7 +2746,8 @@ rb_iseq_only_optparam_p(const rb_iseq_t *iseq) ISEQ_BODY(iseq)->param.flags.has_kwrest == FALSE && ISEQ_BODY(iseq)->param.flags.accepts_no_kwarg == FALSE && ISEQ_BODY(iseq)->param.flags.forwardable == FALSE && - ISEQ_BODY(iseq)->param.flags.has_block == FALSE; + ISEQ_BODY(iseq)->param.flags.has_block == FALSE && + ISEQ_BODY(iseq)->param.flags.accepts_no_block == FALSE; } bool @@ -2757,7 +2759,8 @@ rb_iseq_only_kwparam_p(const rb_iseq_t *iseq) ISEQ_BODY(iseq)->param.flags.has_kw == TRUE && ISEQ_BODY(iseq)->param.flags.has_kwrest == FALSE && ISEQ_BODY(iseq)->param.flags.forwardable == FALSE && - ISEQ_BODY(iseq)->param.flags.has_block == FALSE; + ISEQ_BODY(iseq)->param.flags.has_block == FALSE && + ISEQ_BODY(iseq)->param.flags.accepts_no_block == FALSE; } #define ALLOW_HEAP_ARGV (-2) diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index f178e76728..e1e162c0c2 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1079,6 +1079,39 @@ impl rb_iseq_constant_body_rb_iseq_parameters__bindgen_ty_1 { } } #[inline] + pub fn accepts_no_block(&self) -> ::std::os::raw::c_uint { + unsafe { ::std::mem::transmute(self._bitfield_1.get(14usize, 1u8) as u32) } + } + #[inline] + pub fn set_accepts_no_block(&mut self, val: ::std::os::raw::c_uint) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + self._bitfield_1.set(14usize, 1u8, val as u64) + } + } + #[inline] + pub unsafe fn accepts_no_block_raw(this: *const Self) -> ::std::os::raw::c_uint { + unsafe { + ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 2usize]>>::raw_get( + ::std::ptr::addr_of!((*this)._bitfield_1), + 14usize, + 1u8, + ) as u32) + } + } + #[inline] + pub unsafe fn set_accepts_no_block_raw(this: *mut Self, val: ::std::os::raw::c_uint) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + <__BindgenBitfieldUnit<[u8; 2usize]>>::raw_set( + ::std::ptr::addr_of_mut!((*this)._bitfield_1), + 14usize, + 1u8, + val as u64, + ) + } + } + #[inline] pub fn new_bitfield_1( has_lead: ::std::os::raw::c_uint, has_opt: ::std::os::raw::c_uint, @@ -1094,6 +1127,7 @@ impl rb_iseq_constant_body_rb_iseq_parameters__bindgen_ty_1 { anon_kwrest: ::std::os::raw::c_uint, use_block: ::std::os::raw::c_uint, forwardable: ::std::os::raw::c_uint, + accepts_no_block: ::std::os::raw::c_uint, ) -> __BindgenBitfieldUnit<[u8; 2usize]> { let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 2usize]> = Default::default(); __bindgen_bitfield_unit.set(0usize, 1u8, { @@ -1152,6 +1186,10 @@ impl rb_iseq_constant_body_rb_iseq_parameters__bindgen_ty_1 { let forwardable: u32 = unsafe { ::std::mem::transmute(forwardable) }; forwardable as u64 }); + __bindgen_bitfield_unit.set(14usize, 1u8, { + let accepts_no_block: u32 = unsafe { ::std::mem::transmute(accepts_no_block) }; + accepts_no_block as u64 + }); __bindgen_bitfield_unit } } |
