From fa7cddc969f1eccbb377cfc752bbf82ee2887dde Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 12 Dec 2025 14:53:17 +0900 Subject: [PATCH 01/10] Add Binding#implicit_parameters, etc. This changeset introduces: * `Binding#implicit_parameters` * `Binding#implicit_parameter_get` * `Binding#implicit_parameter_defined?` [Bug #21049] --- proc.c | 86 +++++++++++++++++++++++++++++++++++++++--- test/ruby/test_proc.rb | 54 ++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/proc.c b/proc.c index 1a57757d846ceb..ac56218e7aaa85 100644 --- a/proc.c +++ b/proc.c @@ -409,7 +409,7 @@ bind_eval(int argc, VALUE *argv, VALUE bindval) } static const VALUE * -get_local_variable_ptr(const rb_env_t **envp, ID lid) +get_local_variable_ptr(const rb_env_t **envp, ID lid, bool search_outer) { const rb_env_t *env = *envp; do { @@ -446,7 +446,7 @@ get_local_variable_ptr(const rb_env_t **envp, ID lid) *envp = NULL; return NULL; } - } while ((env = rb_vm_env_prev_env(env)) != NULL); + } while (search_outer && (env = rb_vm_env_prev_env(env)) != NULL); *envp = NULL; return NULL; @@ -548,7 +548,7 @@ bind_local_variable_get(VALUE bindval, VALUE sym) GetBindingPtr(bindval, bind); env = VM_ENV_ENVVAL_PTR(vm_block_ep(&bind->block)); - if ((ptr = get_local_variable_ptr(&env, lid)) != NULL) { + if ((ptr = get_local_variable_ptr(&env, lid, TRUE)) != NULL) { return *ptr; } @@ -600,7 +600,7 @@ bind_local_variable_set(VALUE bindval, VALUE sym, VALUE val) GetBindingPtr(bindval, bind); env = VM_ENV_ENVVAL_PTR(vm_block_ep(&bind->block)); - if ((ptr = get_local_variable_ptr(&env, lid)) == NULL) { + if ((ptr = get_local_variable_ptr(&env, lid, TRUE)) == NULL) { /* not found. create new env */ ptr = rb_binding_add_dynavars(bindval, bind, 1, &lid); env = VM_ENV_ENVVAL_PTR(vm_block_ep(&bind->block)); @@ -647,7 +647,80 @@ bind_local_variable_defined_p(VALUE bindval, VALUE sym) GetBindingPtr(bindval, bind); env = VM_ENV_ENVVAL_PTR(vm_block_ep(&bind->block)); - return RBOOL(get_local_variable_ptr(&env, lid)); + return RBOOL(get_local_variable_ptr(&env, lid, TRUE)); +} + +/* + * call-seq: + * binding.implicit_parameters -> Array + * + * TODO + */ +static VALUE +bind_implicit_parameters(VALUE bindval) +{ + const rb_binding_t *bind; + const rb_env_t *env; + + // TODO: it + + GetBindingPtr(bindval, bind); + env = VM_ENV_ENVVAL_PTR(vm_block_ep(&bind->block)); + return rb_vm_env_numbered_parameters(env); +} + +/* + * call-seq: + * binding.implicit_parameter_get(symbol) -> obj + * + * TODO + */ +static VALUE +bind_implicit_parameter_get(VALUE bindval, VALUE sym) +{ + ID lid = check_local_id(bindval, &sym); + const rb_binding_t *bind; + const VALUE *ptr; + const rb_env_t *env; + + if (!lid || !rb_numparam_id_p(lid)) { + rb_name_err_raise("'%1$s' is not an implicit parameter", + bindval, ID2SYM(lid)); + } + + GetBindingPtr(bindval, bind); + + env = VM_ENV_ENVVAL_PTR(vm_block_ep(&bind->block)); + if ((ptr = get_local_variable_ptr(&env, lid, FALSE)) != NULL) { + return *ptr; + } + + rb_name_err_raise("implicit parameter '%1$s' is not defined for %2$s", bindval, ID2SYM(lid)); + UNREACHABLE_RETURN(Qundef); +} + +/* + * call-seq: + * binding.implicit_parameter_defined?(symbol) -> obj + * + * TODO + * + */ +static VALUE +bind_implicit_parameter_defined_p(VALUE bindval, VALUE sym) +{ + ID lid = check_local_id(bindval, &sym); + const rb_binding_t *bind; + const rb_env_t *env; + + if (!lid || !rb_numparam_id_p(lid)) { + rb_name_err_raise("'%1$s' is not an implicit parameter", + bindval, ID2SYM(lid)); + } + + GetBindingPtr(bindval, bind); + env = VM_ENV_ENVVAL_PTR(vm_block_ep(&bind->block)); + return RBOOL(get_local_variable_ptr(&env, lid, FALSE)); } /* @@ -4607,6 +4680,9 @@ Init_Binding(void) rb_define_method(rb_cBinding, "local_variable_get", bind_local_variable_get, 1); rb_define_method(rb_cBinding, "local_variable_set", bind_local_variable_set, 2); rb_define_method(rb_cBinding, "local_variable_defined?", bind_local_variable_defined_p, 1); + rb_define_method(rb_cBinding, "implicit_parameters", bind_implicit_parameters, 0); + rb_define_method(rb_cBinding, "implicit_parameter_get", bind_implicit_parameter_get, 1); + rb_define_method(rb_cBinding, "implicit_parameter_defined?", bind_implicit_parameter_defined_p, 1); rb_define_method(rb_cBinding, "receiver", bind_receiver, 0); rb_define_method(rb_cBinding, "source_location", bind_location, 0); rb_define_global_function("binding", rb_f_binding, 0); diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb index 2eeb7a94ebb706..d6b2d804698644 100644 --- a/test/ruby/test_proc.rb +++ b/test/ruby/test_proc.rb @@ -1691,6 +1691,60 @@ def test_numparam_is_not_local_variables end end + def test_implicit_parameters + x = x = 1 + assert_raise(NameError) { binding.implicit_parameter_get(:x) } + assert_raise(NameError) { binding.implicit_parameter_defined?(:x) } + + "foo".tap do + _5 and flunk + assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:_1)) + assert_equal(nil, binding.implicit_parameter_get(:_5)) + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(true, binding.implicit_parameter_defined?(:_1)) + assert_equal(true, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + "bar".tap do + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + end + assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:_1)) + assert_equal(nil, binding.implicit_parameter_get(:_5)) + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(true, binding.implicit_parameter_defined?(:_1)) + assert_equal(true, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + end + + "foo".tap do + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + "bar".tap do + _5 and flunk + assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) + assert_equal("bar", binding.implicit_parameter_get(:_1)) + assert_equal(nil, binding.implicit_parameter_get(:_5)) + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(true, binding.implicit_parameter_defined?(:_1)) + assert_equal(true, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + end + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + end + end + def test_it_is_not_local_variable "foo".tap do it From 129d74c96b0b8f1b93704b34e66d2c18f4835e1b Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 12 Dec 2025 16:43:25 +0900 Subject: [PATCH 02/10] Binding#implicit_parameters, etc. support the implicit "it" parameter [Bug #21049] --- defs/id.def | 2 + iseq.c | 13 +++-- parse.y | 4 +- prism_compile.c | 3 +- proc.c | 23 ++++++-- test/ruby/test_proc.rb | 129 ++++++++++++++++++++++++++++++++++++++++- 6 files changed, 159 insertions(+), 15 deletions(-) diff --git a/defs/id.def b/defs/id.def index 0c32b0d1d4ab97..cd3a8480a241df 100644 --- a/defs/id.def +++ b/defs/id.def @@ -78,6 +78,8 @@ firstline, predefined = __LINE__+1, %[\ _7 NUMPARAM_7 _8 NUMPARAM_8 _9 NUMPARAM_9 + ItImplicit + it It "/*NULL*/" NULL empty? diff --git a/iseq.c b/iseq.c index b53362b2444709..726501d45cd27e 100644 --- a/iseq.c +++ b/iseq.c @@ -19,6 +19,7 @@ #endif #include "eval_intern.h" +#include "id.h" #include "id_table.h" #include "internal.h" #include "internal/bits.h" @@ -3363,7 +3364,7 @@ iseq_data_to_ary(const rb_iseq_t *iseq) for (i=0; ilocal_table_size; i++) { ID lid = iseq_body->local_table[i]; if (lid) { - if (rb_id2str(lid)) { + if (lid != idItImplicit && rb_id2str(lid)) { rb_ary_push(locals, ID2SYM(lid)); } else { /* hidden variable from id_internal() */ @@ -3673,10 +3674,10 @@ rb_iseq_parameters(const rb_iseq_t *iseq, int is_proc) ID req, opt, rest, block, key, keyrest; #define PARAM_TYPE(type) rb_ary_push(a = rb_ary_new2(2), ID2SYM(type)) #define PARAM_ID(i) body->local_table[(i)] -#define PARAM(i, type) ( \ - PARAM_TYPE(type), \ - rb_id2str(PARAM_ID(i)) ? \ - rb_ary_push(a, ID2SYM(PARAM_ID(i))) : \ +#define PARAM(i, type) ( \ + PARAM_TYPE(type), \ + PARAM_ID(i) != idItImplicit && rb_id2str(PARAM_ID(i)) ? \ + rb_ary_push(a, ID2SYM(PARAM_ID(i))) : \ a) CONST_ID(req, "req"); @@ -3695,7 +3696,7 @@ rb_iseq_parameters(const rb_iseq_t *iseq, int is_proc) if (is_proc) { for (i = 0; i < body->param.lead_num; i++) { PARAM_TYPE(opt); - if (rb_id2str(PARAM_ID(i))) { + if (PARAM_ID(i) != idItImplicit && rb_id2str(PARAM_ID(i))) { rb_ary_push(a, ID2SYM(PARAM_ID(i))); } rb_ary_push(args, a); diff --git a/parse.y b/parse.y index 2a6de39236dc4c..496dc21b11baec 100644 --- a/parse.y +++ b/parse.y @@ -13035,14 +13035,14 @@ gettable(struct parser_params *p, ID id, const YYLTYPE *loc) } # endif /* method call without arguments */ - if (dyna_in_block(p) && id == rb_intern("it") && !(DVARS_TERMINAL_P(p->lvtbl->args) || DVARS_TERMINAL_P(p->lvtbl->args->prev))) { + if (dyna_in_block(p) && id == idIt && !(DVARS_TERMINAL_P(p->lvtbl->args) || DVARS_TERMINAL_P(p->lvtbl->args->prev))) { if (numparam_used_p(p)) return 0; if (p->max_numparam == ORDINAL_PARAM) { compile_error(p, "ordinary parameter is defined"); return 0; } if (!p->it_id) { - p->it_id = internal_id(p); + p->it_id = idItImplicit; vtable_add(p->lvtbl->args, p->it_id); } NODE *node = NEW_DVAR(p->it_id, loc); diff --git a/prism_compile.c b/prism_compile.c index d3c66691a8325b..77b940ce6c6008 100644 --- a/prism_compile.c +++ b/prism_compile.c @@ -6398,8 +6398,7 @@ pm_compile_scope_node(rb_iseq_t *iseq, pm_scope_node_t *scope_node, const pm_nod } if (scope_node->parameters != NULL && PM_NODE_TYPE_P(scope_node->parameters, PM_IT_PARAMETERS_NODE)) { - ID local = rb_make_temporary_id(local_index); - local_table_for_iseq->ids[local_index++] = local; + local_table_for_iseq->ids[local_index++] = idItImplicit; } // def foo(a, (b, *c, d), e = 1, *f, g, (h, *i, j), k:, l: 1, **m, &n) diff --git a/proc.c b/proc.c index ac56218e7aaa85..01864794e8b0ca 100644 --- a/proc.c +++ b/proc.c @@ -514,6 +514,12 @@ rb_numparam_id_p(ID id) return (tNUMPARAM_1 << ID_SCOPE_SHIFT) <= id && id < ((tNUMPARAM_1 + 9) << ID_SCOPE_SHIFT); } +int +rb_implicit_param_p(ID id) +{ + return id == idItImplicit || rb_numparam_id_p(id); +} + /* * call-seq: * binding.local_variable_get(symbol) -> obj @@ -662,9 +668,13 @@ bind_implicit_parameters(VALUE bindval) const rb_binding_t *bind; const rb_env_t *env; - // TODO: it - GetBindingPtr(bindval, bind); + env = VM_ENV_ENVVAL_PTR(vm_block_ep(&bind->block)); + + if (RBOOL(get_local_variable_ptr(&env, idItImplicit, FALSE))) { + return rb_ary_new_from_args(1, ID2SYM(idIt)); + } + env = VM_ENV_ENVVAL_PTR(vm_block_ep(&bind->block)); return rb_vm_env_numbered_parameters(env); } @@ -683,7 +693,9 @@ bind_implicit_parameter_get(VALUE bindval, VALUE sym) const VALUE *ptr; const rb_env_t *env; - if (!lid || !rb_numparam_id_p(lid)) { + if (lid == idIt) lid = idItImplicit; + + if (!lid || !rb_implicit_param_p(lid)) { rb_name_err_raise("'%1$s' is not an implicit parameter", bindval, ID2SYM(lid)); } @@ -695,6 +707,7 @@ bind_implicit_parameter_get(VALUE bindval, VALUE sym) return *ptr; } + if (lid == idItImplicit) lid = idIt; rb_name_err_raise("implicit parameter '%1$s' is not defined for %2$s", bindval, ID2SYM(lid)); UNREACHABLE_RETURN(Qundef); } @@ -713,7 +726,9 @@ bind_implicit_parameter_defined_p(VALUE bindval, VALUE sym) const rb_binding_t *bind; const rb_env_t *env; - if (!lid || !rb_numparam_id_p(lid)) { + if (lid == idIt) lid = idItImplicit; + + if (!lid || !rb_implicit_param_p(lid)) { rb_name_err_raise("'%1$s' is not an implicit parameter", bindval, ID2SYM(lid)); } diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb index d6b2d804698644..9ac1875234814b 100644 --- a/test/ruby/test_proc.rb +++ b/test/ruby/test_proc.rb @@ -1691,7 +1691,7 @@ def test_numparam_is_not_local_variables end end - def test_implicit_parameters + def test_implicit_parameters_for_numparams x = x = 1 assert_raise(NameError) { binding.implicit_parameter_get(:x) } assert_raise(NameError) { binding.implicit_parameter_defined?(:x) } @@ -1702,23 +1702,29 @@ def test_implicit_parameters assert_equal("foo", binding.implicit_parameter_get(:_1)) assert_equal(nil, binding.implicit_parameter_get(:_5)) assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } assert_equal(true, binding.implicit_parameter_defined?(:_1)) assert_equal(true, binding.implicit_parameter_defined?(:_5)) assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) "bar".tap do assert_equal([], binding.implicit_parameters) assert_raise(NameError) { binding.implicit_parameter_get(:_1) } assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } assert_equal(false, binding.implicit_parameter_defined?(:_1)) assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) end assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) assert_equal("foo", binding.implicit_parameter_get(:_1)) assert_equal(nil, binding.implicit_parameter_get(:_5)) assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } assert_equal(true, binding.implicit_parameter_defined?(:_1)) assert_equal(true, binding.implicit_parameter_defined?(:_5)) assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) end "foo".tap do @@ -1727,21 +1733,25 @@ def test_implicit_parameters assert_raise(NameError) { binding.implicit_parameter_get(:_6) } assert_equal(false, binding.implicit_parameter_defined?(:_1)) assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) "bar".tap do _5 and flunk assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) assert_equal("bar", binding.implicit_parameter_get(:_1)) assert_equal(nil, binding.implicit_parameter_get(:_5)) assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_raise(NameError) { binding.implicit_parameter_get(:it) } assert_equal(true, binding.implicit_parameter_defined?(:_1)) assert_equal(true, binding.implicit_parameter_defined?(:_5)) assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) end assert_equal([], binding.implicit_parameters) assert_raise(NameError) { binding.implicit_parameter_get(:_1) } assert_raise(NameError) { binding.implicit_parameter_get(:_6) } assert_equal(false, binding.implicit_parameter_defined?(:_1)) assert_equal(false, binding.implicit_parameter_defined?(:_6)) + assert_equal(false, binding.implicit_parameter_defined?(:it)) end end @@ -1786,6 +1796,123 @@ def test_it_is_not_local_variable end end + def test_implicit_parameters_for_it + "foo".tap do + it or flunk + assert_equal([:it], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:it)) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(true, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + "bar".tap do + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(false, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + end + assert_equal([:it], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:it)) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(true, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + end + + "foo".tap do + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(false, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + "bar".tap do + it or flunk + assert_equal([:it], binding.implicit_parameters) + assert_equal("bar", binding.implicit_parameter_get(:it)) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(true, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + end + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_equal(false, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + end + end + + def test_implicit_parameters_for_it_complex + "foo".tap do + it = "bar" + + assert_equal([], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal(false, binding.implicit_parameter_defined?(:it)) + + assert_equal([:it], binding.local_variables) + assert_equal("bar", binding.local_variable_get(:it)) + assert_equal(true, binding.local_variable_defined?(:it)) + end + + "foo".tap do + it or flunk + + assert_equal([:it], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:it)) + assert_equal(true, binding.implicit_parameter_defined?(:it)) + + assert_equal([], binding.local_variables) + assert_raise(NameError) { binding.local_variable_get(:it) } + assert_equal(false, binding.local_variable_defined?(:it)) + end + + "foo".tap do + it or flunk + it = "bar" + + assert_equal([:it], binding.implicit_parameters) + assert_equal("foo", binding.implicit_parameter_get(:it)) + assert_equal(true, binding.implicit_parameter_defined?(:it)) + + assert_equal([:it], binding.local_variables) + assert_equal("bar", binding.local_variable_get(:it)) + assert_equal(true, binding.local_variable_defined?(:it)) + end + end + + def test_implicit_parameters_for_it_and_numparams + "foo".tap do + it or flunk + "bar".tap do + _5 and flunk + assert_equal([:_1, :_2, :_3, :_4, :_5], binding.implicit_parameters) + assert_raise(NameError) { binding.implicit_parameter_get(:it) } + assert_equal("bar", binding.implicit_parameter_get(:_1)) + assert_equal(nil, binding.implicit_parameter_get(:_5)) + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(false, binding.implicit_parameter_defined?(:it)) + assert_equal(true, binding.implicit_parameter_defined?(:_1)) + assert_equal(true, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + end + end + + "foo".tap do + _5 and flunk + "bar".tap do + it or flunk + assert_equal([:it], binding.implicit_parameters) + assert_equal("bar", binding.implicit_parameter_get(:it)) + assert_raise(NameError) { binding.implicit_parameter_get(:_1) } + assert_raise(NameError) { binding.implicit_parameter_get(:_5) } + assert_raise(NameError) { binding.implicit_parameter_get(:_6) } + assert_equal(true, binding.implicit_parameter_defined?(:it)) + assert_equal(false, binding.implicit_parameter_defined?(:_1)) + assert_equal(false, binding.implicit_parameter_defined?(:_5)) + assert_equal(false, binding.implicit_parameter_defined?(:_6)) + end + end + end + def test_local_variable_set_wb assert_ruby_status([], <<-'end;', '[Bug #13605]', timeout: 30) b = binding From 04422384ddf57159321f7fb214c457a378f65837 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 12 Dec 2025 16:52:26 +0900 Subject: [PATCH 03/10] Add docs to Binding#numbered_parameters, etc. --- proc.c | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/proc.c b/proc.c index 01864794e8b0ca..23b6bf1a9a6f4d 100644 --- a/proc.c +++ b/proc.c @@ -660,7 +660,21 @@ bind_local_variable_defined_p(VALUE bindval, VALUE sym) * call-seq: * binding.implicit_parameters -> Array * - * TODO + * Returns the names of numbered parameters and "it" parameter + * that are defined in the binding. + * + * def foo + * [42].each do + * it + * binding.implicit_parameters #=> [:it] + * end + * + * { k: 42 }.each do + * _2 + * binding.implicit_parameters #=> [:_1, :_2] + * end + * end + * */ static VALUE bind_implicit_parameters(VALUE bindval) @@ -683,7 +697,21 @@ bind_implicit_parameters(VALUE bindval) * call-seq: * binding.implicit_parameter_get(symbol) -> obj * - * TODO + * Returns the value of the numbered parameter or "it" parameter. + * + * def foo + * [42].each do + * it + * binding.implicit_parameter_get(:it) #=> 42 + * end + * + * { k: 42 }.each do + * _2 + * binding.implicit_parameter_get(:_1) #=> :k + * binding.implicit_parameter_get(:_2) #=> 42 + * end + * end + * */ static VALUE bind_implicit_parameter_get(VALUE bindval, VALUE sym) @@ -716,7 +744,23 @@ bind_implicit_parameter_get(VALUE bindval, VALUE sym) * call-seq: * binding.implicit_parameter_defined?(symbol) -> obj * - * TODO + * Returns +true+ if the numbered parameter or "it" parameter exists. + * + * def foo + * [42].each do + * it + * binding.implicit_parameter_defined?(:it) #=> true + * binding.implicit_parameter_defined?(:_1) #=> false + * end + * + * { k: 42 }.each do + * _2 + * binding.implicit_parameter_defined?(:_1) #=> true + * binding.implicit_parameter_defined?(:_2) #=> true + * binding.implicit_parameter_defined?(:_3) #=> false + * binding.implicit_parameter_defined?(:it) #=> false + * end + * end * */ static VALUE From f939cf40ba46f3a8136495702793748fa30c12c3 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Fri, 12 Dec 2025 22:54:31 +0900 Subject: [PATCH 04/10] Update NEWS about `Binding#implicit_parameters`, etc. [Bug #21049] --- NEWS.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index cd214a365b4597..09fc053ef0b478 100644 --- a/NEWS.md +++ b/NEWS.md @@ -61,8 +61,13 @@ Note: We're only listing outstanding class updates. * Binding * `Binding#local_variables` does no longer include numbered parameters. - Also, `Binding#local_variable_get` and `Binding#local_variable_set` reject - to handle numbered parameters. [[Bug #21049]] + Also, `Binding#local_variable_get`, `Binding#local_variable_set`, and + `Binding#local_variable_defined?` reject to handle numbered parameters. + [[Bug #21049]] + + * `Binding#implicit_parameters`, `Binding#implicit_parameter_get`, and + `Binding#implicit_parameter_defined?` have been added to access + numbered parameters and "it" parameter. [[Bug #21049]] * File From b8ba9cebb9472c125c946e36ea4455e7aae284a6 Mon Sep 17 00:00:00 2001 From: tomoya ishida Date: Sat, 13 Dec 2025 01:40:49 +0900 Subject: [PATCH 05/10] Fix binding.implicit_parameters_get/defined segfault when wrong name string is passed (#15530) --- proc.c | 4 ++-- test/ruby/test_proc.rb | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/proc.c b/proc.c index 23b6bf1a9a6f4d..9cd4c5b0c9f488 100644 --- a/proc.c +++ b/proc.c @@ -725,7 +725,7 @@ bind_implicit_parameter_get(VALUE bindval, VALUE sym) if (!lid || !rb_implicit_param_p(lid)) { rb_name_err_raise("'%1$s' is not an implicit parameter", - bindval, ID2SYM(lid)); + bindval, sym); } GetBindingPtr(bindval, bind); @@ -774,7 +774,7 @@ bind_implicit_parameter_defined_p(VALUE bindval, VALUE sym) if (!lid || !rb_implicit_param_p(lid)) { rb_name_err_raise("'%1$s' is not an implicit parameter", - bindval, ID2SYM(lid)); + bindval, sym); } GetBindingPtr(bindval, bind); diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb index 9ac1875234814b..92cdfc6757da22 100644 --- a/test/ruby/test_proc.rb +++ b/test/ruby/test_proc.rb @@ -1913,6 +1913,14 @@ def test_implicit_parameters_for_it_and_numparams end end + def test_implicit_parameter_invalid_name + message_pattern = /is not an implicit parameter/ + assert_raise_with_message(NameError, message_pattern) { binding.implicit_parameter_defined?(:foo) } + assert_raise_with_message(NameError, message_pattern) { binding.implicit_parameter_get(:foo) } + assert_raise_with_message(NameError, message_pattern) { binding.implicit_parameter_defined?("wrong_implicit_parameter_name_#{rand(10000)}") } + assert_raise_with_message(NameError, message_pattern) { binding.implicit_parameter_get("wrong_implicit_parameter_name_#{rand(10000)}") } + end + def test_local_variable_set_wb assert_ruby_status([], <<-'end;', '[Bug #13605]', timeout: 30) b = binding From bb4a6f39519c101b6bdc847d11cad55f19e62e9a Mon Sep 17 00:00:00 2001 From: Ryan Davis Date: Sun, 7 Dec 2025 01:11:04 -0800 Subject: [PATCH 06/10] [ruby/prism] Fixed Prism::Translation::RubyParser's comment processing Tests were failing in Flay under Prism. https://github.com/ruby/prism/commit/af9b3640a8 --- lib/prism/translation/ruby_parser.rb | 62 +++++++++++++++++++++------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/lib/prism/translation/ruby_parser.rb b/lib/prism/translation/ruby_parser.rb index 2ca7da0bf2a5d5..5149756addefe4 100644 --- a/lib/prism/translation/ruby_parser.rb +++ b/lib/prism/translation/ruby_parser.rb @@ -415,14 +415,18 @@ def visit_class_node(node) visit(node.constant_path) end - if node.body.nil? - s(node, :class, name, visit(node.superclass)) - elsif node.body.is_a?(StatementsNode) - compiler = copy_compiler(in_def: false) - s(node, :class, name, visit(node.superclass)).concat(node.body.body.map { |child| child.accept(compiler) }) - else - s(node, :class, name, visit(node.superclass), node.body.accept(copy_compiler(in_def: false))) - end + result = + if node.body.nil? + s(node, :class, name, visit(node.superclass)) + elsif node.body.is_a?(StatementsNode) + compiler = copy_compiler(in_def: false) + s(node, :class, name, visit(node.superclass)).concat(node.body.body.map { |child| child.accept(compiler) }) + else + s(node, :class, name, visit(node.superclass), node.body.accept(copy_compiler(in_def: false))) + end + + attach_comments(result, node) + result end # ``` @@ -611,7 +615,9 @@ def visit_def_node(node) s(node, :defs, visit(node.receiver), name) end + attach_comments(result, node) result.line(node.name_loc.start_line) + if node.parameters.nil? result << s(node, :args).line(node.name_loc.start_line) else @@ -1270,14 +1276,18 @@ def visit_module_node(node) visit(node.constant_path) end - if node.body.nil? - s(node, :module, name) - elsif node.body.is_a?(StatementsNode) - compiler = copy_compiler(in_def: false) - s(node, :module, name).concat(node.body.body.map { |child| child.accept(compiler) }) - else - s(node, :module, name, node.body.accept(copy_compiler(in_def: false))) - end + result = + if node.body.nil? + s(node, :module, name) + elsif node.body.is_a?(StatementsNode) + compiler = copy_compiler(in_def: false) + s(node, :module, name).concat(node.body.body.map { |child| child.accept(compiler) }) + else + s(node, :module, name, node.body.accept(copy_compiler(in_def: false))) + end + + attach_comments(result, node) + result end # ``` @@ -1820,6 +1830,17 @@ def visit_yield_node(node) private + # Attach prism comments to the given sexp. + def attach_comments(sexp, node) + return unless node.comments + return if node.comments.empty? + + extra = node.location.start_line - node.comments.last.location.start_line + comments = node.comments.map(&:slice) + comments.concat([nil] * [0, extra].max) + sexp.comments = comments.join("\n") + end + # Create a new compiler with the given options. def copy_compiler(in_def: self.in_def, in_pattern: self.in_pattern) Compiler.new(file, in_def: in_def, in_pattern: in_pattern) @@ -1898,6 +1919,14 @@ def parse_file(filepath) translate(Prism.parse_file(filepath, partial_script: true), filepath) end + # Parse the give file and translate it into the + # seattlerb/ruby_parser gem's Sexp format. This method is + # provided for API compatibility to RubyParser and takes an + # optional +timeout+ argument. + def process(ruby, file = "(string)", timeout = nil) + Timeout.timeout(timeout) { parse(ruby, file) } + end + class << self # Parse the given source and translate it into the seattlerb/ruby_parser # gem's Sexp format. @@ -1922,6 +1951,7 @@ def translate(result, filepath) raise ::RubyParser::SyntaxError, "#{filepath}:#{error.location.start_line} :: #{error.message}" end + result.attach_comments! result.value.accept(Compiler.new(filepath)) end end From 3a0596b9a51121ebb7d3db06c438bf4182507c2a Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Fri, 12 Dec 2025 13:14:00 -0500 Subject: [PATCH 07/10] ZJIT: Add Shape type to HIR (#15528) It's just a nicety (they fit fine as CUInt32) but this makes printing look nicer in real execution and also in tests (helps with #15489). Co-authored-by: Randy Stauner --- zjit/src/codegen.rs | 4 ++ zjit/src/hir.rs | 9 ++-- zjit/src/hir/opt_tests.rs | 26 +++++------ zjit/src/hir_type/gen_hir_type.rb | 1 + zjit/src/hir_type/hir_type.inc.rs | 71 ++++++++++++++++--------------- zjit/src/hir_type/mod.rs | 7 +++ 6 files changed, 68 insertions(+), 50 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index dbcffb04273a75..8c7d9a3b4a66ee 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -364,6 +364,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::Const { val: Const::CInt64(val) } => gen_const_long(val), &Insn::Const { val: Const::CUInt16(val) } => gen_const_uint16(val), &Insn::Const { val: Const::CUInt32(val) } => gen_const_uint32(val), + &Insn::Const { val: Const::CShape(val) } => { + assert_eq!(SHAPE_ID_NUM_BITS, 32); + gen_const_uint32(val.0) + } Insn::Const { .. } => panic!("Unexpected Const in gen_insn: {insn}"), Insn::NewArray { elements, state } => gen_new_array(asm, opnds!(elements), &function.frame_state(*state)), Insn::NewHash { elements, state } => gen_new_hash(jit, asm, opnds!(elements), &function.frame_state(*state)), diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 1cdc5e003a7cf2..859454bac5f324 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -303,6 +303,7 @@ pub enum Const { CUInt8(u8), CUInt16(u16), CUInt32(u32), + CShape(ShapeId), CUInt64(u64), CPtr(*const u8), CDouble(f64), @@ -389,6 +390,7 @@ impl<'a> std::fmt::Display for ConstPrinter<'a> { // number than {:?} does and we don't know why. // We'll have to resolve that first. Const::CPtr(val) => write!(f, "CPtr({:?})", self.ptr_map.map_ptr(val)), + &Const::CShape(shape_id) => write!(f, "CShape({:p})", self.ptr_map.map_shape(shape_id)), _ => write!(f, "{:?}", self.inner), } } @@ -468,7 +470,7 @@ impl PtrPrintMap { } /// Map shape ID into a pointer for printing - fn map_shape(&self, id: ShapeId) -> *const c_void { + pub fn map_shape(&self, id: ShapeId) -> *const c_void { self.map_ptr(id.0 as *const c_void) } } @@ -2140,6 +2142,7 @@ impl Function { Insn::Const { val: Const::CUInt8(val) } => Type::from_cint(types::CUInt8, *val as i64), Insn::Const { val: Const::CUInt16(val) } => Type::from_cint(types::CUInt16, *val as i64), Insn::Const { val: Const::CUInt32(val) } => Type::from_cint(types::CUInt32, *val as i64), + Insn::Const { val: Const::CShape(val) } => Type::from_cint(types::CShape, val.0 as i64), Insn::Const { val: Const::CUInt64(val) } => Type::from_cint(types::CUInt64, *val as i64), Insn::Const { val: Const::CPtr(val) } => Type::from_cptr(*val), Insn::Const { val: Const::CDouble(val) } => Type::from_double(*val), @@ -3206,8 +3209,7 @@ impl Function { self.push_insn(block, Insn::WriteBarrier { recv: self_val, val }); if next_shape_id != recv_type.shape() { // Write the new shape ID - assert_eq!(SHAPE_ID_NUM_BITS, 32); - let shape_id = self.push_insn(block, Insn::Const { val: Const::CUInt32(next_shape_id.0) }); + let shape_id = self.push_insn(block, Insn::Const { val: Const::CShape(next_shape_id) }); let shape_id_offset = unsafe { rb_shape_id_offset() }; self.push_insn(block, Insn::StoreField { recv: self_val, id: ID!(_shape_id), offset: shape_id_offset, val: shape_id }); } @@ -4773,6 +4775,7 @@ impl Function { Const::CUInt8(_) => self.assert_subtype(insn_id, val, types::CUInt8), Const::CUInt16(_) => self.assert_subtype(insn_id, val, types::CUInt16), Const::CUInt32(_) => self.assert_subtype(insn_id, val, types::CUInt32), + Const::CShape(_) => self.assert_subtype(insn_id, val, types::CShape), Const::CUInt64(_) => self.assert_subtype(insn_id, val, types::CUInt64), Const::CBool(_) => self.assert_subtype(insn_id, val, types::CBool), Const::CDouble(_) => self.assert_subtype(insn_id, val, types::CDouble), diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 3946c9d03c0714..ab5bcf2022b64c 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -3813,8 +3813,8 @@ mod hir_opt_tests { v20:HeapBasicObject = GuardShape v19, 0x1000 StoreField v20, :@foo@0x1001, v10 WriteBarrier v20, v10 - v23:CUInt32[4194311] = Const CUInt32(4194311) - StoreField v20, :_shape_id@0x1002, v23 + v23:CShape[0x1002] = Const CShape(0x1002) + StoreField v20, :_shape_id@0x1003, v23 CheckInterrupts Return v10 "); @@ -3845,16 +3845,16 @@ mod hir_opt_tests { v26:HeapBasicObject = GuardShape v25, 0x1000 StoreField v26, :@foo@0x1001, v10 WriteBarrier v26, v10 - v29:CUInt32[4194311] = Const CUInt32(4194311) - StoreField v26, :_shape_id@0x1002, v29 + v29:CShape[0x1002] = Const CShape(0x1002) + StoreField v26, :_shape_id@0x1003, v29 v16:Fixnum[2] = Const Value(2) PatchPoint SingleRactorMode v31:HeapBasicObject = GuardType v6, HeapBasicObject - v32:HeapBasicObject = GuardShape v31, 0x1003 + v32:HeapBasicObject = GuardShape v31, 0x1002 StoreField v32, :@bar@0x1004, v16 WriteBarrier v32, v16 - v35:CUInt32[4194312] = Const CUInt32(4194312) - StoreField v32, :_shape_id@0x1002, v35 + v35:CShape[0x1005] = Const CShape(0x1005) + StoreField v32, :_shape_id@0x1003, v35 CheckInterrupts Return v16 "); @@ -6095,8 +6095,8 @@ mod hir_opt_tests { v29:HeapObject[class_exact:C] = GuardShape v26, 0x1038 StoreField v29, :@foo@0x1039, v16 WriteBarrier v29, v16 - v32:CUInt32[4194311] = Const CUInt32(4194311) - StoreField v29, :_shape_id@0x103a, v32 + v32:CShape[0x103a] = Const CShape(0x103a) + StoreField v29, :_shape_id@0x103b, v32 CheckInterrupts Return v16 "); @@ -6130,8 +6130,8 @@ mod hir_opt_tests { v29:HeapObject[class_exact:C] = GuardShape v26, 0x1038 StoreField v29, :@foo@0x1039, v16 WriteBarrier v29, v16 - v32:CUInt32[4194311] = Const CUInt32(4194311) - StoreField v29, :_shape_id@0x103a, v32 + v32:CShape[0x103a] = Const CShape(0x103a) + StoreField v29, :_shape_id@0x103b, v32 CheckInterrupts Return v16 "); @@ -9514,8 +9514,8 @@ mod hir_opt_tests { v57:HeapBasicObject = GuardShape v56, 0x1000 StoreField v57, :@formatted@0x1001, v39 WriteBarrier v57, v39 - v60:CUInt32[4194311] = Const CUInt32(4194311) - StoreField v57, :_shape_id@0x1002, v60 + v60:CShape[0x1002] = Const CShape(0x1002) + StoreField v57, :_shape_id@0x1003, v60 v45:Class[VMFrozenCore] = Const Value(VALUE(0x1008)) PatchPoint MethodRedefined(Class@0x1010, lambda@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Class@0x1010) diff --git a/zjit/src/hir_type/gen_hir_type.rb b/zjit/src/hir_type/gen_hir_type.rb index e51d0c04e1a324..9576d2b1c06f19 100644 --- a/zjit/src/hir_type/gen_hir_type.rb +++ b/zjit/src/hir_type/gen_hir_type.rb @@ -139,6 +139,7 @@ def final_type name, base: $object, c_name: nil signed.subtype "CInt#{width}" unsigned.subtype "CUInt#{width}" } +unsigned.subtype "CShape" # Assign individual bits to type leaves and union bit patterns to nodes with subtypes num_bits = 0 diff --git a/zjit/src/hir_type/hir_type.inc.rs b/zjit/src/hir_type/hir_type.inc.rs index a330440612a739..b388b3a0d10780 100644 --- a/zjit/src/hir_type/hir_type.inc.rs +++ b/zjit/src/hir_type/hir_type.inc.rs @@ -19,58 +19,59 @@ mod bits { pub const CInt8: u64 = 1u64 << 10; pub const CNull: u64 = 1u64 << 11; pub const CPtr: u64 = 1u64 << 12; + pub const CShape: u64 = 1u64 << 13; pub const CSigned: u64 = CInt16 | CInt32 | CInt64 | CInt8; - pub const CUInt16: u64 = 1u64 << 13; - pub const CUInt32: u64 = 1u64 << 14; - pub const CUInt64: u64 = 1u64 << 15; - pub const CUInt8: u64 = 1u64 << 16; - pub const CUnsigned: u64 = CUInt16 | CUInt32 | CUInt64 | CUInt8; + pub const CUInt16: u64 = 1u64 << 14; + pub const CUInt32: u64 = 1u64 << 15; + pub const CUInt64: u64 = 1u64 << 16; + pub const CUInt8: u64 = 1u64 << 17; + pub const CUnsigned: u64 = CShape | CUInt16 | CUInt32 | CUInt64 | CUInt8; pub const CValue: u64 = CBool | CDouble | CInt | CNull | CPtr; - pub const CallableMethodEntry: u64 = 1u64 << 17; - pub const Class: u64 = 1u64 << 18; - pub const DynamicSymbol: u64 = 1u64 << 19; + pub const CallableMethodEntry: u64 = 1u64 << 18; + pub const Class: u64 = 1u64 << 19; + pub const DynamicSymbol: u64 = 1u64 << 20; pub const Empty: u64 = 0u64; - pub const FalseClass: u64 = 1u64 << 20; - pub const Fixnum: u64 = 1u64 << 21; + pub const FalseClass: u64 = 1u64 << 21; + pub const Fixnum: u64 = 1u64 << 22; pub const Float: u64 = Flonum | HeapFloat; - pub const Flonum: u64 = 1u64 << 22; + pub const Flonum: u64 = 1u64 << 23; pub const Hash: u64 = HashExact | HashSubclass; - pub const HashExact: u64 = 1u64 << 23; - pub const HashSubclass: u64 = 1u64 << 24; + pub const HashExact: u64 = 1u64 << 24; + pub const HashSubclass: u64 = 1u64 << 25; pub const HeapBasicObject: u64 = BasicObject & !Immediate; - pub const HeapFloat: u64 = 1u64 << 25; + pub const HeapFloat: u64 = 1u64 << 26; pub const HeapObject: u64 = Object & !Immediate; pub const Immediate: u64 = FalseClass | Fixnum | Flonum | NilClass | StaticSymbol | TrueClass | Undef; pub const Integer: u64 = Bignum | Fixnum; pub const Module: u64 = Class | ModuleExact | ModuleSubclass; - pub const ModuleExact: u64 = 1u64 << 26; - pub const ModuleSubclass: u64 = 1u64 << 27; - pub const NilClass: u64 = 1u64 << 28; + pub const ModuleExact: u64 = 1u64 << 27; + pub const ModuleSubclass: u64 = 1u64 << 28; + pub const NilClass: u64 = 1u64 << 29; pub const Numeric: u64 = Float | Integer | NumericExact | NumericSubclass; - pub const NumericExact: u64 = 1u64 << 29; - pub const NumericSubclass: u64 = 1u64 << 30; + pub const NumericExact: u64 = 1u64 << 30; + pub const NumericSubclass: u64 = 1u64 << 31; pub const Object: u64 = Array | FalseClass | Hash | Module | NilClass | Numeric | ObjectExact | ObjectSubclass | Range | Regexp | Set | String | Symbol | TrueClass; - pub const ObjectExact: u64 = 1u64 << 31; - pub const ObjectSubclass: u64 = 1u64 << 32; + pub const ObjectExact: u64 = 1u64 << 32; + pub const ObjectSubclass: u64 = 1u64 << 33; pub const Range: u64 = RangeExact | RangeSubclass; - pub const RangeExact: u64 = 1u64 << 33; - pub const RangeSubclass: u64 = 1u64 << 34; + pub const RangeExact: u64 = 1u64 << 34; + pub const RangeSubclass: u64 = 1u64 << 35; pub const Regexp: u64 = RegexpExact | RegexpSubclass; - pub const RegexpExact: u64 = 1u64 << 35; - pub const RegexpSubclass: u64 = 1u64 << 36; + pub const RegexpExact: u64 = 1u64 << 36; + pub const RegexpSubclass: u64 = 1u64 << 37; pub const RubyValue: u64 = BasicObject | CallableMethodEntry | Undef; pub const Set: u64 = SetExact | SetSubclass; - pub const SetExact: u64 = 1u64 << 37; - pub const SetSubclass: u64 = 1u64 << 38; - pub const StaticSymbol: u64 = 1u64 << 39; + pub const SetExact: u64 = 1u64 << 38; + pub const SetSubclass: u64 = 1u64 << 39; + pub const StaticSymbol: u64 = 1u64 << 40; pub const String: u64 = StringExact | StringSubclass; - pub const StringExact: u64 = 1u64 << 40; - pub const StringSubclass: u64 = 1u64 << 41; + pub const StringExact: u64 = 1u64 << 41; + pub const StringSubclass: u64 = 1u64 << 42; pub const Subclass: u64 = ArraySubclass | BasicObjectSubclass | HashSubclass | ModuleSubclass | NumericSubclass | ObjectSubclass | RangeSubclass | RegexpSubclass | SetSubclass | StringSubclass; pub const Symbol: u64 = DynamicSymbol | StaticSymbol; - pub const TrueClass: u64 = 1u64 << 42; - pub const Undef: u64 = 1u64 << 43; - pub const AllBitPatterns: [(&str, u64); 70] = [ + pub const TrueClass: u64 = 1u64 << 43; + pub const Undef: u64 = 1u64 << 44; + pub const AllBitPatterns: [(&str, u64); 71] = [ ("Any", Any), ("RubyValue", RubyValue), ("Immediate", Immediate), @@ -125,6 +126,7 @@ mod bits { ("CUInt64", CUInt64), ("CUInt32", CUInt32), ("CUInt16", CUInt16), + ("CShape", CShape), ("CPtr", CPtr), ("CNull", CNull), ("CSigned", CSigned), @@ -142,7 +144,7 @@ mod bits { ("ArrayExact", ArrayExact), ("Empty", Empty), ]; - pub const NumTypeBits: u64 = 44; + pub const NumTypeBits: u64 = 45; } pub mod types { use super::*; @@ -165,6 +167,7 @@ pub mod types { pub const CInt8: Type = Type::from_bits(bits::CInt8); pub const CNull: Type = Type::from_bits(bits::CNull); pub const CPtr: Type = Type::from_bits(bits::CPtr); + pub const CShape: Type = Type::from_bits(bits::CShape); pub const CSigned: Type = Type::from_bits(bits::CSigned); pub const CUInt16: Type = Type::from_bits(bits::CUInt16); pub const CUInt32: Type = Type::from_bits(bits::CUInt32); diff --git a/zjit/src/hir_type/mod.rs b/zjit/src/hir_type/mod.rs index 206d94f42bdb64..c87f1313b577d3 100644 --- a/zjit/src/hir_type/mod.rs +++ b/zjit/src/hir_type/mod.rs @@ -92,6 +92,8 @@ fn write_spec(f: &mut std::fmt::Formatter, printer: &TypePrinter) -> std::fmt::R Specialization::Int(val) if ty.is_subtype(types::CInt8) => write!(f, "[{}]", (val & u8::MAX as u64) as i8), Specialization::Int(val) if ty.is_subtype(types::CInt16) => write!(f, "[{}]", (val & u16::MAX as u64) as i16), Specialization::Int(val) if ty.is_subtype(types::CInt32) => write!(f, "[{}]", (val & u32::MAX as u64) as i32), + Specialization::Int(val) if ty.is_subtype(types::CShape) => + write!(f, "[{:p}]", printer.ptr_map.map_shape(crate::cruby::ShapeId((val & u32::MAX as u64) as u32))), Specialization::Int(val) if ty.is_subtype(types::CInt64) => write!(f, "[{}]", val as i64), Specialization::Int(val) if ty.is_subtype(types::CUInt8) => write!(f, "[{}]", val & u8::MAX as u64), Specialization::Int(val) if ty.is_subtype(types::CUInt16) => write!(f, "[{}]", val & u16::MAX as u64), @@ -258,6 +260,7 @@ impl Type { Const::CUInt8(v) => Self::from_cint(types::CUInt8, v as i64), Const::CUInt16(v) => Self::from_cint(types::CUInt16, v as i64), Const::CUInt32(v) => Self::from_cint(types::CUInt32, v as i64), + Const::CShape(v) => Self::from_cint(types::CShape, v.0 as i64), Const::CUInt64(v) => Self::from_cint(types::CUInt64, v as i64), Const::CPtr(v) => Self::from_cptr(v), Const::CDouble(v) => Self::from_double(v), @@ -526,6 +529,10 @@ impl Type { if self.is_subtype(types::CUInt8) || self.is_subtype(types::CInt8) { return 1; } if self.is_subtype(types::CUInt16) || self.is_subtype(types::CInt16) { return 2; } if self.is_subtype(types::CUInt32) || self.is_subtype(types::CInt32) { return 4; } + if self.is_subtype(types::CShape) { + use crate::cruby::{SHAPE_ID_NUM_BITS, BITS_PER_BYTE}; + return (SHAPE_ID_NUM_BITS as usize / BITS_PER_BYTE).try_into().unwrap(); + } // CUInt64, CInt64, CPtr, CNull, CDouble, or anything else defaults to 8 bytes crate::cruby::SIZEOF_VALUE as u8 } From 309d6ef9c3792d1116809620a01240c7b8f4406e Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 11 Dec 2025 21:23:37 +0000 Subject: [PATCH 08/10] ZJIT: Inline `Hash#[]=` --- test/ruby/test_zjit.rb | 71 +++++++++++++++++++++++++++++ zjit/src/codegen.rs | 6 +++ zjit/src/cruby_methods.rs | 7 +++ zjit/src/hir.rs | 17 +++++-- zjit/src/hir/opt_tests.rs | 96 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 194 insertions(+), 3 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 232c46079f5b38..50ffb6138c34bd 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -1429,6 +1429,77 @@ def test = {}.freeze }, insns: [:opt_hash_freeze], call_threshold: 1 end + def test_opt_aset_hash + assert_compiles '42', %q{ + def test(h, k, v) + h[k] = v + end + h = {} + test(h, :key, 42) + test(h, :key, 42) + h[:key] + }, call_threshold: 2, insns: [:opt_aset] + end + + def test_opt_aset_hash_returns_value + assert_compiles '100', %q{ + def test(h, k, v) + h[k] = v + end + test({}, :key, 100) + test({}, :key, 100) + }, call_threshold: 2 + end + + def test_opt_aset_hash_string_key + assert_compiles '"bar"', %q{ + def test(h, k, v) + h[k] = v + end + h = {} + test(h, "foo", "bar") + test(h, "foo", "bar") + h["foo"] + }, call_threshold: 2 + end + + def test_opt_aset_hash_subclass + assert_compiles '42', %q{ + class MyHash < Hash; end + def test(h, k, v) + h[k] = v + end + h = MyHash.new + test(h, :key, 42) + test(h, :key, 42) + h[:key] + }, call_threshold: 2 + end + + def test_opt_aset_hash_too_few_args + assert_compiles '"ArgumentError"', %q{ + def test(h) + h.[]= 123 + rescue ArgumentError + "ArgumentError" + end + test({}) + test({}) + }, call_threshold: 2 + end + + def test_opt_aset_hash_too_many_args + assert_compiles '"ArgumentError"', %q{ + def test(h) + h[:a, :b] = :c + rescue ArgumentError + "ArgumentError" + end + test({}) + test({}) + }, call_threshold: 2 + end + def test_opt_ary_freeze assert_compiles "[[], 5]", %q{ def test = [].freeze diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 8c7d9a3b4a66ee..67f53a3477c099 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -484,6 +484,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::CheckInterrupts { state } => no_output!(gen_check_interrupts(jit, asm, &function.frame_state(state))), &Insn::HashDup { val, state } => { gen_hash_dup(asm, opnd!(val), &function.frame_state(state)) }, &Insn::HashAref { hash, key, state } => { gen_hash_aref(jit, asm, opnd!(hash), opnd!(key), &function.frame_state(state)) }, + &Insn::HashAset { hash, key, val, state } => { no_output!(gen_hash_aset(jit, asm, opnd!(hash), opnd!(key), opnd!(val), &function.frame_state(state))) }, &Insn::ArrayPush { array, val, state } => { no_output!(gen_array_push(asm, opnd!(array), opnd!(val), &function.frame_state(state))) }, &Insn::ToNewArray { val, state } => { gen_to_new_array(jit, asm, opnd!(val), &function.frame_state(state)) }, &Insn::ToArray { val, state } => { gen_to_array(jit, asm, opnd!(val), &function.frame_state(state)) }, @@ -1065,6 +1066,11 @@ fn gen_hash_aref(jit: &mut JITState, asm: &mut Assembler, hash: Opnd, key: Opnd, asm_ccall!(asm, rb_hash_aref, hash, key) } +fn gen_hash_aset(jit: &mut JITState, asm: &mut Assembler, hash: Opnd, key: Opnd, val: Opnd, state: &FrameState) { + gen_prepare_non_leaf_call(jit, asm, state); + asm_ccall!(asm, rb_hash_aset, hash, key, val); +} + fn gen_array_push(asm: &mut Assembler, array: Opnd, val: Opnd, state: &FrameState) { gen_prepare_leaf_call_with_gc(asm, state); asm_ccall!(asm, rb_ary_push, array, val); diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 71bf6ab83df0ad..14fced81acaab0 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -229,6 +229,7 @@ pub fn init() -> Annotations { annotate!(rb_cArray, "push", inline_array_push); annotate!(rb_cArray, "pop", inline_array_pop); annotate!(rb_cHash, "[]", inline_hash_aref); + annotate!(rb_cHash, "[]=", inline_hash_aset); annotate!(rb_cHash, "size", types::Fixnum, no_gc, leaf, elidable); annotate!(rb_cHash, "empty?", types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cNilClass, "nil?", inline_nilclass_nil_p); @@ -356,6 +357,12 @@ fn inline_hash_aref(fun: &mut hir::Function, block: hir::BlockId, recv: hir::Ins None } +fn inline_hash_aset(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { + let &[key, val] = args else { return None; }; + let _ = fun.push_insn(block, hir::Insn::HashAset { hash: recv, key, val, state }); + // Hash#[]= returns the value, not the hash + Some(val) +} fn inline_string_bytesize(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { if args.is_empty() && fun.likely_a(recv, types::String, state) { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 859454bac5f324..05cbc07d8802eb 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -733,6 +733,7 @@ pub enum Insn { ArrayLength { array: InsnId }, HashAref { hash: InsnId, key: InsnId, state: InsnId }, + HashAset { hash: InsnId, key: InsnId, val: InsnId, state: InsnId }, HashDup { val: InsnId, state: InsnId }, /// Allocate an instance of the `val` object without calling `#initialize` on it. @@ -991,7 +992,8 @@ impl Insn { | Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. } | Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetGlobal { .. } | Insn::SetLocal { .. } | Insn::Throw { .. } | Insn::IncrCounter(_) | Insn::IncrCounterPtr { .. } - | Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::StoreField { .. } | Insn::WriteBarrier { .. } => false, + | Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::StoreField { .. } | Insn::WriteBarrier { .. } + | Insn::HashAset { .. } => false, _ => true, } } @@ -1182,6 +1184,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::ArrayDup { val, .. } => { write!(f, "ArrayDup {val}") } Insn::HashDup { val, .. } => { write!(f, "HashDup {val}") } Insn::HashAref { hash, key, .. } => { write!(f, "HashAref {hash}, {key}")} + Insn::HashAset { hash, key, val, .. } => { write!(f, "HashAset {hash}, {key}, {val}")} Insn::ObjectAlloc { val, .. } => { write!(f, "ObjectAlloc {val}") } &Insn::ObjectAllocClass { class, .. } => { let class_name = get_class_name(class); @@ -2034,6 +2037,7 @@ impl Function { &ArrayDup { val, state } => ArrayDup { val: find!(val), state }, &HashDup { val, state } => HashDup { val: find!(val), state }, &HashAref { hash, key, state } => HashAref { hash: find!(hash), key: find!(key), state }, + &HashAset { hash, key, val, state } => HashAset { hash: find!(hash), key: find!(key), val: find!(val), state }, &ObjectAlloc { val, state } => ObjectAlloc { val: find!(val), state }, &ObjectAllocClass { class, state } => ObjectAllocClass { class, state: find!(state) }, &CCall { cfunc, recv, ref args, name, return_type, elidable } => CCall { cfunc, recv: find!(recv), args: find_vec!(args), name, return_type, elidable }, @@ -2131,7 +2135,7 @@ impl Function { | Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. } | Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetLocal { .. } | Insn::IncrCounter(_) | Insn::CheckInterrupts { .. } | Insn::GuardBlockParamProxy { .. } | Insn::IncrCounterPtr { .. } - | Insn::StoreField { .. } | Insn::WriteBarrier { .. } => + | Insn::StoreField { .. } | Insn::WriteBarrier { .. } | Insn::HashAset { .. } => panic!("Cannot infer type of instruction with no output: {}. See Insn::has_output().", self.insns[insn.0]), Insn::Const { val: Const::Value(val) } => Type::from_value(*val), Insn::Const { val: Const::CBool(val) } => Type::from_cbool(*val), @@ -4004,6 +4008,12 @@ impl Function { worklist.push_back(key); worklist.push_back(state); } + &Insn::HashAset { hash, key, val, state } => { + worklist.push_back(hash); + worklist.push_back(key); + worklist.push_back(val); + worklist.push_back(state); + } &Insn::Send { recv, ref args, state, .. } | &Insn::SendForward { recv, ref args, state, .. } | &Insn::SendWithoutBlock { recv, ref args, state, .. } @@ -4699,7 +4709,8 @@ impl Function { self.assert_subtype(insn_id, index, types::Fixnum) } // Instructions with Hash operands - Insn::HashAref { hash, .. } => self.assert_subtype(insn_id, hash, types::Hash), + Insn::HashAref { hash, .. } + | Insn::HashAset { hash, .. } => self.assert_subtype(insn_id, hash, types::Hash), Insn::HashDup { val, .. } => self.assert_subtype(insn_id, val, types::HashExact), // Other Insn::ObjectAllocClass { class, .. } => { diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index ab5bcf2022b64c..3e751a1bbd1778 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6738,6 +6738,102 @@ mod hir_opt_tests { "); } + #[test] + fn test_hash_aset_literal() { + eval(" + def test + h = {} + h[1] = 3 + end + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:NilClass = Const Value(nil) + Jump bb2(v1, v2) + bb1(v5:BasicObject): + EntryPoint JIT(0) + v6:NilClass = Const Value(nil) + Jump bb2(v5, v6) + bb2(v8:BasicObject, v9:NilClass): + v13:HashExact = NewHash + PatchPoint NoEPEscape(test) + v22:Fixnum[1] = Const Value(1) + v24:Fixnum[3] = Const Value(3) + PatchPoint MethodRedefined(Hash@0x1000, []=@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(Hash@0x1000) + HashAset v13, v22, v24 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_hash_aset_profiled() { + eval(" + def test(hash, key, val) + hash[key] = val + end + test({}, 0, 1) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :hash, l0, SP@6 + v3:BasicObject = GetLocal :key, l0, SP@5 + v4:BasicObject = GetLocal :val, l0, SP@4 + Jump bb2(v1, v2, v3, v4) + bb1(v7:BasicObject, v8:BasicObject, v9:BasicObject, v10:BasicObject): + EntryPoint JIT(0) + Jump bb2(v7, v8, v9, v10) + bb2(v12:BasicObject, v13:BasicObject, v14:BasicObject, v15:BasicObject): + PatchPoint MethodRedefined(Hash@0x1000, []=@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(Hash@0x1000) + v35:HashExact = GuardType v13, HashExact + HashAset v35, v14, v15 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v15 + "); + } + + #[test] + fn test_hash_aset_subclass() { + eval(" + class C < Hash; end + def test(hash, key, val) + hash[key] = val + end + test(C.new, 0, 1) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:4: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :hash, l0, SP@6 + v3:BasicObject = GetLocal :key, l0, SP@5 + v4:BasicObject = GetLocal :val, l0, SP@4 + Jump bb2(v1, v2, v3, v4) + bb1(v7:BasicObject, v8:BasicObject, v9:BasicObject, v10:BasicObject): + EntryPoint JIT(0) + Jump bb2(v7, v8, v9, v10) + bb2(v12:BasicObject, v13:BasicObject, v14:BasicObject, v15:BasicObject): + PatchPoint MethodRedefined(C@0x1000, []=@0x1008, cme:0x1010) + PatchPoint NoSingletonClass(C@0x1000) + v35:HashSubclass[class_exact:C] = GuardType v13, HashSubclass[class_exact:C] + HashAset v35, v14, v15 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v15 + "); + } + #[test] fn test_optimize_thread_current() { eval(" From 4f900e3ce9cefa76bc2c94e0e591acd51bf0e638 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Fri, 12 Dec 2025 17:42:40 +0000 Subject: [PATCH 09/10] ZJIT: Only optimize `[]` and `[]=` for exact Hash, not Hash subclasses --- zjit/src/cruby_methods.rs | 24 ++++++++++++++++++------ zjit/src/hir.rs | 2 +- zjit/src/hir/opt_tests.rs | 10 ++++------ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 14fced81acaab0..60060f149c850d 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -350,18 +350,30 @@ fn inline_array_pop(fun: &mut hir::Function, block: hir::BlockId, recv: hir::Ins } fn inline_hash_aref(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { - if let &[key] = args { + let &[key] = args else { return None; }; + + // Only optimize exact Hash, not subclasses + if fun.likely_a(recv, types::HashExact, state) { + let recv = fun.coerce_to(block, recv, types::HashExact, state); let result = fun.push_insn(block, hir::Insn::HashAref { hash: recv, key, state }); - return Some(result); + Some(result) + } else { + None } - None } fn inline_hash_aset(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { let &[key, val] = args else { return None; }; - let _ = fun.push_insn(block, hir::Insn::HashAset { hash: recv, key, val, state }); - // Hash#[]= returns the value, not the hash - Some(val) + + // Only optimize exact Hash, not subclasses + if fun.likely_a(recv, types::HashExact, state) { + let recv = fun.coerce_to(block, recv, types::HashExact, state); + let _ = fun.push_insn(block, hir::Insn::HashAset { hash: recv, key, val, state }); + // Hash#[]= returns the value, not the hash + Some(val) + } else { + None + } } fn inline_string_bytesize(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 05cbc07d8802eb..77bd172396fdda 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4710,7 +4710,7 @@ impl Function { } // Instructions with Hash operands Insn::HashAref { hash, .. } - | Insn::HashAset { hash, .. } => self.assert_subtype(insn_id, hash, types::Hash), + | Insn::HashAset { hash, .. } => self.assert_subtype(insn_id, hash, types::HashExact), Insn::HashDup { val, .. } => self.assert_subtype(insn_id, val, types::HashExact), // Other Insn::ObjectAllocClass { class, .. } => { diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 3e751a1bbd1778..4c62971c1eacb1 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6678,7 +6678,7 @@ mod hir_opt_tests { } #[test] - fn test_hash_aref_subclass() { + fn test_no_optimize_hash_aref_subclass() { eval(" class C < Hash; end def test(hash, key) @@ -6701,8 +6701,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(C@0x1000, []@0x1008, cme:0x1010) PatchPoint NoSingletonClass(C@0x1000) v27:HashSubclass[class_exact:C] = GuardType v11, HashSubclass[class_exact:C] - v28:BasicObject = HashAref v27, v12 - IncrCounter inline_cfunc_optimized_send_count + v28:BasicObject = CCallWithFrame v27, :Hash#[]@0x1038, v12 CheckInterrupts Return v28 "); @@ -6803,7 +6802,7 @@ mod hir_opt_tests { } #[test] - fn test_hash_aset_subclass() { + fn test_no_optimize_hash_aset_subclass() { eval(" class C < Hash; end def test(hash, key, val) @@ -6827,8 +6826,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(C@0x1000, []=@0x1008, cme:0x1010) PatchPoint NoSingletonClass(C@0x1000) v35:HashSubclass[class_exact:C] = GuardType v13, HashSubclass[class_exact:C] - HashAset v35, v14, v15 - IncrCounter inline_cfunc_optimized_send_count + v36:BasicObject = CCallWithFrame v35, :Hash#[]=@0x1038, v14, v15 CheckInterrupts Return v15 "); From 6147b695870ce82ee3ad5305ce095b63889b8d9d Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 14 Nov 2025 11:31:23 -0500 Subject: [PATCH 10/10] Array#rfind Implement Array#rfind, which is the same as find except from the other side of the Array. Also implemented Array#find (as opposed to the generic one on Enumerable because it is significantly faster and to keep the implementations together. [Feature #21678] --- array.c | 95 +++++++++++++++++++++++++++++++++++++++++ test/ruby/test_array.rb | 17 ++++++++ 2 files changed, 112 insertions(+) diff --git a/array.c b/array.c index a6aeeeeca156b4..1352b5c046ae1d 100644 --- a/array.c +++ b/array.c @@ -2088,6 +2088,99 @@ rb_ary_fetch(int argc, VALUE *argv, VALUE ary) return RARRAY_AREF(ary, idx); } +/* + * call-seq: + * find(if_none_proc = nil) {|element| ... } -> object or nil + * find(if_none_proc = nil) -> enumerator + * + * Returns the first element for which the block returns a truthy value. + * + * With a block given, calls the block with successive elements of the array; + * returns the first element for which the block returns a truthy value: + * + * (0..9).find {|element| element > 2} # => 3 + * + * If no such element is found, calls +if_none_proc+ and returns its return value. + * + * (0..9).find(proc {false}) {|element| element > 12} # => false + * {foo: 0, bar: 1, baz: 2}.find {|key, value| key.start_with?('b') } # => [:bar, 1] + * {foo: 0, bar: 1, baz: 2}.find(proc {[]}) {|key, value| key.start_with?('c') } # => [] + * + * With no block given, returns an Enumerator. + * + */ + +static VALUE +rb_ary_find(int argc, VALUE *argv, VALUE ary) +{ + VALUE if_none; + long idx; + + RETURN_ENUMERATOR(ary, argc, argv); + if_none = rb_check_arity(argc, 0, 1) ? argv[0] : Qnil; + + for (idx = 0; idx < RARRAY_LEN(ary); idx++) { + VALUE elem = RARRAY_AREF(ary, idx); + if (RTEST(rb_yield(elem))) { + return elem; + } + } + + if (!NIL_P(if_none)) { + return rb_funcallv(if_none, idCall, 0, 0); + } + return Qnil; +} + +/* + * call-seq: + * rfind(if_none_proc = nil) {|element| ... } -> object or nil + * rfind(if_none_proc = nil) -> enumerator + * + * Returns the last element for which the block returns a truthy value. + * + * With a block given, calls the block with successive elements of the array in + * reverse order; returns the last element for which the block returns a truthy + * value: + * + * (0..9).rfind {|element| element < 5} # => 4 + * + * If no such element is found, calls +if_none_proc+ and returns its return value. + * + * (0..9).rfind(proc {false}) {|element| element < -2} # => false + * {foo: 0, bar: 1, baz: 2}.rfind {|key, value| key.start_with?('b') } # => [:baz, 2] + * {foo: 0, bar: 1, baz: 2}.rfind(proc {[]}) {|key, value| key.start_with?('c') } # => [] + * + * With no block given, returns an Enumerator. + * + */ + +static VALUE +rb_ary_rfind(int argc, VALUE *argv, VALUE ary) +{ + VALUE if_none; + long len, idx; + + RETURN_ENUMERATOR(ary, argc, argv); + if_none = rb_check_arity(argc, 0, 1) ? argv[0] : Qnil; + + idx = RARRAY_LEN(ary); + while (idx--) { + VALUE elem = RARRAY_AREF(ary, idx); + if (RTEST(rb_yield(elem))) { + return elem; + } + + len = RARRAY_LEN(ary); + idx = (idx >= len) ? len : idx; + } + + if (!NIL_P(if_none)) { + return rb_funcallv(if_none, idCall, 0, 0); + } + return Qnil; +} + /* * call-seq: * find_index(object) -> integer or nil @@ -8816,6 +8909,8 @@ Init_Array(void) rb_define_method(rb_cArray, "length", rb_ary_length, 0); rb_define_method(rb_cArray, "size", rb_ary_length, 0); rb_define_method(rb_cArray, "empty?", rb_ary_empty_p, 0); + rb_define_method(rb_cArray, "find", rb_ary_find, -1); + rb_define_method(rb_cArray, "rfind", rb_ary_rfind, -1); rb_define_method(rb_cArray, "find_index", rb_ary_index, -1); rb_define_method(rb_cArray, "index", rb_ary_index, -1); rb_define_method(rb_cArray, "rindex", rb_ary_rindex, -1); diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index a3ac0a6a0b420d..d93b86e7953b1a 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -3584,6 +3584,23 @@ def test_array_safely_modified_by_sort_block assert_equal((1..67).to_a.reverse, var_0) end + def test_find + ary = [1, 2, 3, 4, 5] + assert_equal(2, ary.find {|x| x % 2 == 0 }) + assert_equal(nil, ary.find {|x| false }) + assert_equal(:foo, ary.find(proc { :foo }) {|x| false }) + end + + def test_rfind + ary = [1, 2, 3, 4, 5] + assert_equal(4, ary.rfind {|x| x % 2 == 0 }) + assert_equal(1, ary.rfind {|x| x < 2 }) + assert_equal(5, ary.rfind {|x| x > 4 }) + assert_equal(nil, ary.rfind {|x| false }) + assert_equal(:foo, ary.rfind(proc { :foo }) {|x| false }) + assert_equal(nil, ary.rfind {|x| ary.clear; false }) + end + private def need_continuation unless respond_to?(:callcc, true)