summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNobuyoshi Nakada <nobu@ruby-lang.org>2024-02-20 19:34:50 +0900
committerKevin Newton <kddnewton@gmail.com>2026-02-15 14:12:15 -0500
commit116d402067c1d01ed67b16ec93fb523bd5109d07 (patch)
tree9ed0f81052e64934218586ae691dd4d2d1fbec8d
parent2065b55980a0fc6386d58e4ede37d60c50e5b62f (diff)
[Feature #19979] Method definition with `&nil`
Allow methods to declare that they don't accept a block via `&nil`.
-rw-r--r--compile.c11
-rw-r--r--parse.y9
-rw-r--r--rubyparser.h1
-rw-r--r--spec/ruby/language/block_spec.rb11
-rw-r--r--spec/ruby/language/method_spec.rb12
-rw-r--r--test/ruby/test_syntax.rb43
-rw-r--r--vm_args.c23
-rw-r--r--vm_core.h1
-rw-r--r--vm_insnhelper.c9
-rw-r--r--zjit/src/cruby_bindings.inc.rs38
10 files changed, 149 insertions, 9 deletions
diff --git a/compile.c b/compile.c
index 5d6cfb1e16..c9806947fd 100644
--- a/compile.c
+++ b/compile.c
@@ -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;
diff --git a/parse.y b/parse.y
index b874e4f8dc..6e0c610a7c 100644
--- a/parse.y
+++ b/parse.y
@@ -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|
diff --git a/vm_args.c b/vm_args.c
index 90a18ee8df..62c63caa98 100644
--- a/vm_args.c
+++ b/vm_args.c
@@ -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);
}
diff --git a/vm_core.h b/vm_core.h
index 5f0266da1d..7fcf8ca5c1 100644
--- a/vm_core.h
+++ b/vm_core.h
@@ -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
}
}