From dba4c9fbe79bdc3b7679330760d2b27d29a9911a Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 1 Nov 2025 10:49:14 -0400 Subject: [PATCH 01/15] Fix string allocation when slot size < 40 bytes We need to allocate at least sizeof(struct RString) when the string is embedded on garbage collectors that support slot sizes less than 40 bytes. --- string.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/string.c b/string.c index fa6ce7f3ec1d07..2a250cc69da130 100644 --- a/string.c +++ b/string.c @@ -242,7 +242,9 @@ rb_str_reembeddable_p(VALUE str) static inline size_t rb_str_embed_size(long capa) { - return offsetof(struct RString, as.embed.ary) + capa; + size_t size = offsetof(struct RString, as.embed.ary) + capa; + if (size < sizeof(struct RString)) size = sizeof(struct RString); + return size; } size_t From 2380f69f6875709ee4c37095cea4e6163d9faac1 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 1 Nov 2025 10:51:04 -0400 Subject: [PATCH 02/15] Fix array allocation when slot size < 40 bytes We need to allocate at least sizeof(struct RArray) when the array is embedded on garbage collectors that support slot sizes less than 40 bytes. --- array.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/array.c b/array.c index 12f45e2cbb421a..b71123532d19cc 100644 --- a/array.c +++ b/array.c @@ -194,7 +194,9 @@ ary_embed_capa(VALUE ary) static size_t ary_embed_size(long capa) { - return offsetof(struct RArray, as.ary) + (sizeof(VALUE) * capa); + size_t size = offsetof(struct RArray, as.ary) + (sizeof(VALUE) * capa); + if (size < sizeof(struct RArray)) size = sizeof(struct RArray); + return size; } static bool From 37c7153668b31edbf56ec6227a7dc30cdcc45e4f Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 1 Nov 2025 11:17:46 -0400 Subject: [PATCH 03/15] Make rb_str_embed_size aware of termlen --- string.c | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/string.c b/string.c index 2a250cc69da130..8b18ad97709712 100644 --- a/string.c +++ b/string.c @@ -240,9 +240,9 @@ rb_str_reembeddable_p(VALUE str) } static inline size_t -rb_str_embed_size(long capa) +rb_str_embed_size(long capa, long termlen) { - size_t size = offsetof(struct RString, as.embed.ary) + capa; + size_t size = offsetof(struct RString, as.embed.ary) + capa + termlen; if (size < sizeof(struct RString)) size = sizeof(struct RString); return size; } @@ -252,28 +252,30 @@ rb_str_size_as_embedded(VALUE str) { size_t real_size; if (STR_EMBED_P(str)) { - real_size = rb_str_embed_size(RSTRING(str)->len) + TERM_LEN(str); + size_t capa = RSTRING(str)->len; + if (FL_TEST_RAW(str, STR_PRECOMPUTED_HASH)) capa += sizeof(st_index_t); + + real_size = rb_str_embed_size(capa, TERM_LEN(str)); } /* if the string is not currently embedded, but it can be embedded, how * much space would it require */ else if (rb_str_reembeddable_p(str)) { - real_size = rb_str_embed_size(RSTRING(str)->as.heap.aux.capa) + TERM_LEN(str); + size_t capa = RSTRING(str)->as.heap.aux.capa; + if (FL_TEST_RAW(str, STR_PRECOMPUTED_HASH)) capa += sizeof(st_index_t); + + real_size = rb_str_embed_size(capa, TERM_LEN(str)); } else { real_size = sizeof(struct RString); } - if (FL_TEST_RAW(str, STR_PRECOMPUTED_HASH)) { - real_size += sizeof(st_index_t); - } - return real_size; } static inline bool STR_EMBEDDABLE_P(long len, long termlen) { - return rb_gc_size_allocatable_p(rb_str_embed_size(len + termlen)); + return rb_gc_size_allocatable_p(rb_str_embed_size(len, termlen)); } static VALUE str_replace_shared_without_enc(VALUE str2, VALUE str); @@ -1006,7 +1008,7 @@ must_not_null(const char *ptr) static inline VALUE str_alloc_embed(VALUE klass, size_t capa) { - size_t size = rb_str_embed_size(capa); + size_t size = rb_str_embed_size(capa, 0); RUBY_ASSERT(size > 0); RUBY_ASSERT(rb_gc_size_allocatable_p(size)); @@ -1883,7 +1885,7 @@ str_replace(VALUE str, VALUE str2) static inline VALUE ec_str_alloc_embed(struct rb_execution_context_struct *ec, VALUE klass, size_t capa) { - size_t size = rb_str_embed_size(capa); + size_t size = rb_str_embed_size(capa, 0); RUBY_ASSERT(size > 0); RUBY_ASSERT(rb_gc_size_allocatable_p(size)); From ee7ce9e6d71c7707d13541e9baa3a59745ac639b Mon Sep 17 00:00:00 2001 From: Kazuki Tsujimoto Date: Sun, 2 Nov 2025 23:38:02 +0900 Subject: [PATCH 04/15] Update power_assert to 3.0.0 --- gems/bundled_gems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/bundled_gems b/gems/bundled_gems index 80c93ea007b704..3ff5e3f6b013f4 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -7,7 +7,7 @@ # if `revision` is not given, "v"+`version` or `version` will be used. minitest 5.26.0 https://github.com/minitest/minitest -power_assert 2.0.5 https://github.com/ruby/power_assert f88e406e7c9e0810cc149869582afbae1fb84c4a +power_assert 3.0.0 https://github.com/ruby/power_assert rake 13.3.1 https://github.com/ruby/rake test-unit 3.7.0 https://github.com/test-unit/test-unit rexml 3.4.4 https://github.com/ruby/rexml From 11583f53b172ead33355330c1445964d5af47f46 Mon Sep 17 00:00:00 2001 From: git Date: Sun, 2 Nov 2025 14:39:07 +0000 Subject: [PATCH 05/15] [DOC] Update bundled gems list at ee7ce9e6d71c7707d13541e9baa3a5 --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 6804fd1bc55f69..ab9c0c32b1f958 100644 --- a/NEWS.md +++ b/NEWS.md @@ -213,6 +213,7 @@ The following bundled gems are added. The following bundled gems are updated. * minitest 5.26.0 +* power_assert 3.0.0 * rake 13.3.1 * test-unit 3.7.0 * rexml 3.4.4 From 5153a2dcb3f22cbfa1037764d491646ed5b127d5 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 30 Oct 2025 10:29:33 +0100 Subject: [PATCH 06/15] [ruby/json] Invoke `as_json` callback for strings with invalid encoding Fix: https://github.com/ruby/json/issues/873 This allow users to encode binary strings if they so wish. e.g. they can use Base64 or similar, or chose to replace invalid characters with something else. https://github.com/ruby/json/commit/b1b16c416f --- ext/json/generator/generator.c | 237 ++++++++++++++++++++------------- test/json/json_coder_test.rb | 65 +++++++++ 2 files changed, 207 insertions(+), 95 deletions(-) diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index 72155abe523358..024a8572726098 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -996,13 +996,12 @@ static inline VALUE vstate_get(struct generate_json_data *data) return data->vstate; } -struct hash_foreach_arg { - VALUE hash; - struct generate_json_data *data; - int first_key_type; - bool first; - bool mixed_keys_encountered; -}; +static VALUE +json_call_as_json(JSON_Generator_State *state, VALUE object, VALUE is_key) +{ + VALUE proc_args[2] = {object, is_key}; + return rb_proc_call_with_block(state->as_json, 2, proc_args, Qnil); +} static VALUE convert_string_subclass(VALUE key) @@ -1019,6 +1018,129 @@ convert_string_subclass(VALUE key) return key_to_s; } +static bool enc_utf8_compatible_p(int enc_idx) +{ + if (enc_idx == usascii_encindex) return true; + if (enc_idx == utf8_encindex) return true; + return false; +} + +static VALUE encode_json_string_try(VALUE str) +{ + return rb_funcall(str, i_encode, 1, Encoding_UTF_8); +} + +static VALUE encode_json_string_rescue(VALUE str, VALUE exception) +{ + raise_generator_error_str(str, rb_funcall(exception, rb_intern("message"), 0)); + return Qundef; +} + +static inline bool valid_json_string_p(VALUE str) +{ + int coderange = rb_enc_str_coderange(str); + + if (RB_LIKELY(coderange == ENC_CODERANGE_7BIT)) { + return true; + } + + if (RB_LIKELY(coderange == ENC_CODERANGE_VALID)) { + return enc_utf8_compatible_p(RB_ENCODING_GET_INLINED(str)); + } + + return false; +} + +static inline VALUE ensure_valid_encoding(struct generate_json_data *data, VALUE str, bool as_json_called, bool is_key) +{ + if (RB_LIKELY(valid_json_string_p(str))) { + return str; + } + + if (!as_json_called && data->state->strict && RTEST(data->state->as_json)) { + VALUE coerced_str = json_call_as_json(data->state, str, Qfalse); + if (coerced_str != str) { + if (RB_TYPE_P(coerced_str, T_STRING)) { + if (!valid_json_string_p(coerced_str)) { + raise_generator_error(str, "source sequence is illegal/malformed utf-8"); + } + } else { + // as_json could return another type than T_STRING + if (is_key) { + raise_generator_error(coerced_str, "%"PRIsVALUE" not allowed as object key in JSON", CLASS_OF(coerced_str)); + } + } + + return coerced_str; + } + } + + if (RB_ENCODING_GET_INLINED(str) == binary_encindex) { + VALUE utf8_string = rb_enc_associate_index(rb_str_dup(str), utf8_encindex); + switch (rb_enc_str_coderange(utf8_string)) { + case ENC_CODERANGE_7BIT: + return utf8_string; + case ENC_CODERANGE_VALID: + // For historical reason, we silently reinterpret binary strings as UTF-8 if it would work. + // TODO: Raise in 3.0.0 + rb_warn("JSON.generate: UTF-8 string passed as BINARY, this will raise an encoding error in json 3.0"); + return utf8_string; + break; + } + } + + return rb_rescue(encode_json_string_try, str, encode_json_string_rescue, str); +} + +static void raw_generate_json_string(FBuffer *buffer, struct generate_json_data *data, VALUE obj) +{ + fbuffer_append_char(buffer, '"'); + + long len; + search_state search; + search.buffer = buffer; + RSTRING_GETMEM(obj, search.ptr, len); + search.cursor = search.ptr; + search.end = search.ptr + len; + +#ifdef HAVE_SIMD + search.matches_mask = 0; + search.has_matches = false; + search.chunk_base = NULL; +#endif /* HAVE_SIMD */ + + switch (rb_enc_str_coderange(obj)) { + case ENC_CODERANGE_7BIT: + case ENC_CODERANGE_VALID: + if (RB_UNLIKELY(data->state->ascii_only)) { + convert_UTF8_to_ASCII_only_JSON(&search, data->state->script_safe ? script_safe_escape_table : ascii_only_escape_table); + } else if (RB_UNLIKELY(data->state->script_safe)) { + convert_UTF8_to_script_safe_JSON(&search); + } else { + convert_UTF8_to_JSON(&search); + } + break; + default: + raise_generator_error(obj, "source sequence is illegal/malformed utf-8"); + break; + } + fbuffer_append_char(buffer, '"'); +} + +static void generate_json_string(FBuffer *buffer, struct generate_json_data *data, VALUE obj) +{ + obj = ensure_valid_encoding(data, obj, false, false); + raw_generate_json_string(buffer, data, obj); +} + +struct hash_foreach_arg { + VALUE hash; + struct generate_json_data *data; + int first_key_type; + bool first; + bool mixed_keys_encountered; +}; + NOINLINE() static void json_inspect_hash_with_mixed_keys(struct hash_foreach_arg *arg) @@ -1035,13 +1157,6 @@ json_inspect_hash_with_mixed_keys(struct hash_foreach_arg *arg) } } -static VALUE -json_call_as_json(JSON_Generator_State *state, VALUE object, VALUE is_key) -{ - VALUE proc_args[2] = {object, is_key}; - return rb_proc_call_with_block(state->as_json, 2, proc_args, Qnil); -} - static int json_object_i(VALUE key, VALUE val, VALUE _arg) { @@ -1107,8 +1222,10 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) break; } + key_to_s = ensure_valid_encoding(data, key_to_s, as_json_called, true); + if (RB_LIKELY(RBASIC_CLASS(key_to_s) == rb_cString)) { - generate_json_string(buffer, data, key_to_s); + raw_generate_json_string(buffer, data, key_to_s); } else { generate_json(buffer, data, key_to_s); } @@ -1191,85 +1308,6 @@ static void generate_json_array(FBuffer *buffer, struct generate_json_data *data fbuffer_append_char(buffer, ']'); } -static inline int enc_utf8_compatible_p(int enc_idx) -{ - if (enc_idx == usascii_encindex) return 1; - if (enc_idx == utf8_encindex) return 1; - return 0; -} - -static VALUE encode_json_string_try(VALUE str) -{ - return rb_funcall(str, i_encode, 1, Encoding_UTF_8); -} - -static VALUE encode_json_string_rescue(VALUE str, VALUE exception) -{ - raise_generator_error_str(str, rb_funcall(exception, rb_intern("message"), 0)); - return Qundef; -} - -static inline VALUE ensure_valid_encoding(VALUE str) -{ - int encindex = RB_ENCODING_GET(str); - VALUE utf8_string; - if (RB_UNLIKELY(!enc_utf8_compatible_p(encindex))) { - if (encindex == binary_encindex) { - utf8_string = rb_enc_associate_index(rb_str_dup(str), utf8_encindex); - switch (rb_enc_str_coderange(utf8_string)) { - case ENC_CODERANGE_7BIT: - return utf8_string; - case ENC_CODERANGE_VALID: - // For historical reason, we silently reinterpret binary strings as UTF-8 if it would work. - // TODO: Raise in 3.0.0 - rb_warn("JSON.generate: UTF-8 string passed as BINARY, this will raise an encoding error in json 3.0"); - return utf8_string; - break; - } - } - - str = rb_rescue(encode_json_string_try, str, encode_json_string_rescue, str); - } - return str; -} - -static void generate_json_string(FBuffer *buffer, struct generate_json_data *data, VALUE obj) -{ - obj = ensure_valid_encoding(obj); - - fbuffer_append_char(buffer, '"'); - - long len; - search_state search; - search.buffer = buffer; - RSTRING_GETMEM(obj, search.ptr, len); - search.cursor = search.ptr; - search.end = search.ptr + len; - -#ifdef HAVE_SIMD - search.matches_mask = 0; - search.has_matches = false; - search.chunk_base = NULL; -#endif /* HAVE_SIMD */ - - switch (rb_enc_str_coderange(obj)) { - case ENC_CODERANGE_7BIT: - case ENC_CODERANGE_VALID: - if (RB_UNLIKELY(data->state->ascii_only)) { - convert_UTF8_to_ASCII_only_JSON(&search, data->state->script_safe ? script_safe_escape_table : ascii_only_escape_table); - } else if (RB_UNLIKELY(data->state->script_safe)) { - convert_UTF8_to_script_safe_JSON(&search); - } else { - convert_UTF8_to_JSON(&search); - } - break; - default: - raise_generator_error(obj, "source sequence is illegal/malformed utf-8"); - break; - } - fbuffer_append_char(buffer, '"'); -} - static void generate_json_fallback(FBuffer *buffer, struct generate_json_data *data, VALUE obj) { VALUE tmp; @@ -1408,7 +1446,16 @@ static void generate_json(FBuffer *buffer, struct generate_json_data *data, VALU break; case T_STRING: if (klass != rb_cString) goto general; - generate_json_string(buffer, data, obj); + + if (RB_LIKELY(valid_json_string_p(obj))) { + raw_generate_json_string(buffer, data, obj); + } else if (as_json_called) { + raise_generator_error(obj, "source sequence is illegal/malformed utf-8"); + } else { + obj = ensure_valid_encoding(data, obj, false, false); + as_json_called = true; + goto start; + } break; case T_SYMBOL: generate_json_symbol(buffer, data, obj); diff --git a/test/json/json_coder_test.rb b/test/json/json_coder_test.rb index fb9d7b30a59a11..83b89a3b13dea9 100755 --- a/test/json/json_coder_test.rb +++ b/test/json/json_coder_test.rb @@ -67,6 +67,71 @@ def test_json_coder_dump_NaN_or_Infinity_loop assert_include error.message, "NaN not allowed in JSON" end + def test_json_coder_string_invalid_encoding + calls = 0 + coder = JSON::Coder.new do |object, is_key| + calls += 1 + object + end + + error = assert_raise JSON::GeneratorError do + coder.dump("\xFF") + end + assert_equal "source sequence is illegal/malformed utf-8", error.message + assert_equal 1, calls + + error = assert_raise JSON::GeneratorError do + coder.dump({ "\xFF" => 1 }) + end + assert_equal "source sequence is illegal/malformed utf-8", error.message + assert_equal 2, calls + + calls = 0 + coder = JSON::Coder.new do |object, is_key| + calls += 1 + object.dup + end + + error = assert_raise JSON::GeneratorError do + coder.dump("\xFF") + end + assert_equal "source sequence is illegal/malformed utf-8", error.message + assert_equal 1, calls + + error = assert_raise JSON::GeneratorError do + coder.dump({ "\xFF" => 1 }) + end + assert_equal "source sequence is illegal/malformed utf-8", error.message + assert_equal 2, calls + + calls = 0 + coder = JSON::Coder.new do |object, is_key| + calls += 1 + object.bytes + end + + assert_equal "[255]", coder.dump("\xFF") + assert_equal 1, calls + + error = assert_raise JSON::GeneratorError do + coder.dump({ "\xFF" => 1 }) + end + assert_equal "Array not allowed as object key in JSON", error.message + assert_equal 2, calls + + calls = 0 + coder = JSON::Coder.new do |object, is_key| + calls += 1 + [object].pack("m") + end + + assert_equal '"/w==\\n"', coder.dump("\xFF") + assert_equal 1, calls + + assert_equal '{"/w==\\n":1}', coder.dump({ "\xFF" => 1 }) + assert_equal 2, calls + end + def test_nesting_recovery coder = JSON::Coder.new ary = [] From bc77a11bd845e4c3d79b990c196fedca37e9ab0b Mon Sep 17 00:00:00 2001 From: Satoshi Tagomori Date: Fri, 31 Oct 2025 23:25:43 +0900 Subject: [PATCH 07/15] Add basic namespace tests --- test/ruby/test_namespace.rb | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/test/ruby/test_namespace.rb b/test/ruby/test_namespace.rb index 5a014ad7148a51..af0d6173f24f7b 100644 --- a/test/ruby/test_namespace.rb +++ b/test/ruby/test_namespace.rb @@ -695,4 +695,76 @@ def test_root_and_main_methods pat = "_ruby_ns_*."+RbConfig::CONFIG["DLEXT"] File.unlink(*Dir.glob(pat, base: tmp).map {|so| "#{tmp}/#{so}"}) end + + def test_basic_namespace_detections + assert_separately([ENV_ENABLE_NAMESPACE], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + ns = Namespace.new + code = <<~EOC + NS1 = Namespace.current + class Foo + NS2 = Namespace.current + NS2_proc = ->(){ NS2 } + NS3_proc = ->(){ Namespace.current } + + def ns4 = Namespace.current + def self.ns5 = NS2 + def self.ns6 = Namespace.current + def self.ns6_proc = ->(){ Namespace.current } + + def self.yield_block = yield + def self.call_block(&b) = b.call + end + NS9 = Foo.new.ns4 + EOC + ns.eval(code) + outer = Namespace.current + assert_equal ns, ns::NS1 # on TOP frame + assert_equal ns, ns::Foo::NS2 # on CLASS frame + assert_equal ns, ns::Foo::NS2_proc.call # proc -> a const on CLASS + assert_equal ns, ns::Foo::NS3_proc.call # proc -> the current + assert_equal ns, ns::Foo.new.ns4 # instance method -> the current + assert_equal ns, ns::Foo.ns5 # singleton method -> a const on CLASS + assert_equal ns, ns::Foo.ns6 # singleton method -> the current + assert_equal ns, ns::Foo.ns6_proc.call # method returns a proc -> the current + + assert_equal outer, ns::Foo.yield_block{ Namespace.current } # method yields + assert_equal outer, ns::Foo.call_block{ Namespace.current } # method calls a block + assert_equal ns, ns::NS9 # on TOP frame, referring a class in the current + end; + end + + def test_loading_extension_libs_in_main_namespace + assert_separately([ENV_ENABLE_NAMESPACE], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + require "prism" + require "optparse" + require "date" + require "time" + require "delegate" + require "singleton" + require "pp" + require "fileutils" + require "tempfile" + require "tmpdir" + require "json" + require "psych" + require "yaml" + require "zlib" + require "open3" + require "ipaddr" + require "net/http" + require "openssl" + require "socket" + require "uri" + require "digest" + require "erb" + require "stringio" + require "monitor" + require "timeout" + require "securerandom" + expected = 1 + assert_equal expected, 1 + end; + end end From e89eecceafea3f8834c6fc4cdd74a7812591cae5 Mon Sep 17 00:00:00 2001 From: Satoshi Tagomori Date: Fri, 31 Oct 2025 23:38:16 +0900 Subject: [PATCH 08/15] No need to call rb_define_class/module_under_id Classes/modules defined in a namespace are defined under ::Object as usual (as without namespaces), and it'll be set into the const_tbl of ::Object. In namespaces, namespace objects' const_tbl is equal to the one of ::Object. So constants of ::Object are just equal to constants of the namespace. That means, top level classes/modules in a namespace can be referred as namespace::KlassName without calling rb_define_class_under_id(). --- class.c | 14 ++------------ test/ruby/test_namespace.rb | 4 ++++ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/class.c b/class.c index 5e50a281d9a9fe..a4249425a13a93 100644 --- a/class.c +++ b/class.c @@ -1605,13 +1605,8 @@ VALUE rb_define_class(const char *name, VALUE super) { VALUE klass; - ID id; - const rb_namespace_t *ns = rb_current_namespace(); + ID id = rb_intern(name); - id = rb_intern(name); - if (NAMESPACE_OPTIONAL_P(ns)) { - return rb_define_class_id_under(ns->ns_object, id, super); - } if (rb_const_defined(rb_cObject, id)) { klass = rb_const_get(rb_cObject, id); if (!RB_TYPE_P(klass, T_CLASS)) { @@ -1723,13 +1718,8 @@ VALUE rb_define_module(const char *name) { VALUE module; - ID id; - const rb_namespace_t *ns = rb_current_namespace(); + ID id = rb_intern(name); - id = rb_intern(name); - if (NAMESPACE_OPTIONAL_P(ns)) { - return rb_define_module_id_under(ns->ns_object, id); - } if (rb_const_defined(rb_cObject, id)) { module = rb_const_get(rb_cObject, id); if (!RB_TYPE_P(module, T_MODULE)) { diff --git a/test/ruby/test_namespace.rb b/test/ruby/test_namespace.rb index af0d6173f24f7b..2b7dd04ac7f933 100644 --- a/test/ruby/test_namespace.rb +++ b/test/ruby/test_namespace.rb @@ -715,6 +715,7 @@ def self.ns6_proc = ->(){ Namespace.current } def self.yield_block = yield def self.call_block(&b) = b.call end + FOO_NAME = Foo.name NS9 = Foo.new.ns4 EOC ns.eval(code) @@ -731,6 +732,9 @@ def self.call_block(&b) = b.call assert_equal outer, ns::Foo.yield_block{ Namespace.current } # method yields assert_equal outer, ns::Foo.call_block{ Namespace.current } # method calls a block assert_equal ns, ns::NS9 # on TOP frame, referring a class in the current + + assert_equal "Foo", ns::FOO_NAME + assert_equal "Foo", ns::Foo.name end; end From bb62a1cf8dffdbaf2b97486e781600023ff1356c Mon Sep 17 00:00:00 2001 From: Satoshi Tagomori Date: Sat, 1 Nov 2025 08:00:18 +0900 Subject: [PATCH 09/15] Add flag to ignore EXPERIMENTAL warnings --- test/ruby/test_namespace.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ruby/test_namespace.rb b/test/ruby/test_namespace.rb index 2b7dd04ac7f933..0525e0f2839c1c 100644 --- a/test/ruby/test_namespace.rb +++ b/test/ruby/test_namespace.rb @@ -183,7 +183,7 @@ def test_proc_defined_in_namespace_refers_module_in_namespace pend unless Namespace.enabled? # require_relative dosn't work well in assert_separately even with __FILE__ and __LINE__ - assert_separately([ENV_ENABLE_NAMESPACE], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}") + assert_separately([ENV_ENABLE_NAMESPACE], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) begin; ns1 = Namespace.new ns1.require(File.join("#{here}", 'namespace/proc_callee')) From 12303e2e2e9634dda5e2b5eec7ae7e2296c9f229 Mon Sep 17 00:00:00 2001 From: Satoshi Tagomori Date: Sat, 1 Nov 2025 08:00:56 +0900 Subject: [PATCH 10/15] Fix use of inappropriate debug flag --- namespace.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/namespace.c b/namespace.c index 0f0230b5fdbd9a..3498a4cc4ae433 100644 --- a/namespace.c +++ b/namespace.c @@ -1084,7 +1084,7 @@ Init_Namespace(void) if (rb_namespace_available()) { rb_include_module(rb_cObject, rb_mNamespaceLoader); -#if RUBY_DEBUG > 0 +#if RUBY_DEBUG rb_define_singleton_method(rb_cNamespace, "root", rb_namespace_s_root, 0); rb_define_singleton_method(rb_cNamespace, "main", rb_namespace_s_main, 0); rb_define_global_function("dump_classext", rb_f_dump_classext, 1); From fa4c04a7f75cef4f5e3c1a92b54eba8316c159a5 Mon Sep 17 00:00:00 2001 From: Satoshi Tagomori Date: Sat, 1 Nov 2025 08:01:35 +0900 Subject: [PATCH 11/15] Use CFUNC namespace only for IFUNC frames, its behavior should be unchanged --- test/ruby/test_namespace.rb | 23 ++++++++++- vm.c | 81 ++++++++++++++++++++++++------------- vm_dump.c | 45 +++++++++++---------- 3 files changed, 97 insertions(+), 52 deletions(-) diff --git a/test/ruby/test_namespace.rb b/test/ruby/test_namespace.rb index 0525e0f2839c1c..6df960f7305b80 100644 --- a/test/ruby/test_namespace.rb +++ b/test/ruby/test_namespace.rb @@ -711,12 +711,26 @@ def ns4 = Namespace.current def self.ns5 = NS2 def self.ns6 = Namespace.current def self.ns6_proc = ->(){ Namespace.current } + def self.ns7 + res = [] + [1,2].chunk{ it.even? }.each do |bool, members| + res << Namespace.current.object_id.to_s + ":" + bool.to_s + ":" + members.map(&:to_s).join(",") + end + res + end def self.yield_block = yield def self.call_block(&b) = b.call end FOO_NAME = Foo.name - NS9 = Foo.new.ns4 + + module Kernel + def foo_namespace = Namespace.current + module_function :foo_namespace + end + + NS_X = Foo.new.ns4 + NS_Y = foo_namespace EOC ns.eval(code) outer = Namespace.current @@ -729,9 +743,14 @@ def self.call_block(&b) = b.call assert_equal ns, ns::Foo.ns6 # singleton method -> the current assert_equal ns, ns::Foo.ns6_proc.call # method returns a proc -> the current + # a block after CFUNC/IFUNC in a method -> the current + assert_equal ["#{ns.object_id}:false:1", "#{ns.object_id}:true:2"], ns::Foo.ns7 + assert_equal outer, ns::Foo.yield_block{ Namespace.current } # method yields assert_equal outer, ns::Foo.call_block{ Namespace.current } # method calls a block - assert_equal ns, ns::NS9 # on TOP frame, referring a class in the current + + assert_equal ns, ns::NS_X # on TOP frame, referring a class in the current + assert_equal ns, ns::NS_Y # on TOP frame, referring Kernel method defined by a CFUNC method assert_equal "Foo", ns::FOO_NAME assert_equal "Foo", ns::Foo.name diff --git a/vm.c b/vm.c index c11ac9f7a10510..32785dbcc8cbca 100644 --- a/vm.c +++ b/vm.c @@ -113,39 +113,62 @@ VM_EP_RUBY_LEP(const rb_execution_context_t *ec, const rb_control_frame_t *curre // rb_vmdebug_namespace_env_dump_raw() simulates this function const VALUE *ep = current_cfp->ep; const rb_control_frame_t * const eocfp = RUBY_VM_END_CONTROL_FRAME(ec); /* end of control frame pointer */ - const rb_control_frame_t *cfp = NULL, *checkpoint_cfp = current_cfp; + const rb_control_frame_t *cfp = current_cfp; + + if (VM_ENV_FRAME_TYPE_P(ep, VM_FRAME_MAGIC_IFUNC)) { + ep = VM_EP_LEP(current_cfp->ep); + /** + * Returns CFUNC frame only in this case. + * + * Usually CFUNC frame doesn't represent the current namespace and it should operate + * the caller namespace. See the example: + * + * # in the main namespace + * module Kernel + * def foo = "foo" + * module_function :foo + * end + * + * In the case above, `module_function` is defined in the root namespace. + * If `module_function` worked in the root namespace, `Kernel#foo` is invisible + * from it and it causes NameError: undefined method `foo` for module `Kernel`. + * + * But in cases of IFUNC (blocks written in C), IFUNC doesn't have its own namespace + * and its local env frame will be CFUNC frame. + * For example, `Enumerator#chunk` calls IFUNC blocks, written as `chunk_i` function. + * + * [1].chunk{ it.even? }.each{ ... } + * + * Before calling the Ruby block `{ it.even? }`, `#chunk` calls `chunk_i` as IFUNC + * to iterate the array's members (it's just like `#each`). + * We expect that `chunk_i` works as expected by the implementation of `#chunk` + * without any overwritten definitions from namespaces. + * So the definitions on IFUNC frames should be equal to the caller CFUNC. + */ + VM_ASSERT(VM_ENV_FRAME_TYPE_P(ep, VM_FRAME_MAGIC_CFUNC)); + return ep; + } - while (!VM_ENV_LOCAL_P(ep) || VM_ENV_FRAME_TYPE_P(ep, VM_FRAME_MAGIC_CFUNC)) { - while (!VM_ENV_LOCAL_P(ep)) { - ep = VM_ENV_PREV_EP(ep); - } - while (VM_ENV_FLAGS(ep, VM_FRAME_FLAG_CFRAME) != 0) { - if (!cfp) { - cfp = rb_vm_search_cf_from_ep(ec, checkpoint_cfp, ep); - VM_NAMESPACE_ASSERT(cfp, "Failed to search cfp from ep"); - VM_NAMESPACE_ASSERT(cfp->ep == ep, "Searched cfp's ep is not equal to ep"); - } - if (!cfp) { - return NULL; - } - VM_NAMESPACE_ASSERT(cfp->ep, "cfp->ep == NULL"); - VM_NAMESPACE_ASSERT(cfp->ep == ep, "cfp->ep != ep"); + while (VM_ENV_FRAME_TYPE_P(ep, VM_FRAME_MAGIC_CFUNC)) { + cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp); - VM_NAMESPACE_ASSERT(!VM_FRAME_FINISHED_P(cfp), "CFUNC frame should not FINISHED"); + VM_NAMESPACE_ASSERT(cfp, "CFUNC should have a valid previous control frame"); + VM_NAMESPACE_ASSERT(cfp < eocfp, "CFUNC should have a valid caller frame"); + if (!cfp || cfp >= eocfp) { + return NULL; + } - cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp); - if (cfp >= eocfp) { - return NULL; - } - VM_NAMESPACE_ASSERT(cfp, "CFUNC should have a valid previous control frame"); - ep = cfp->ep; - if (!ep) { - return NULL; - } + VM_NAMESPACE_ASSERT(cfp->ep, "CFUNC should have a valid caller frame with env"); + ep = cfp->ep; + if (!ep) { + return NULL; } - checkpoint_cfp = cfp; - cfp = NULL; } + + while (!VM_ENV_LOCAL_P(ep)) { + ep = VM_ENV_PREV_EP(ep); + } + return ep; } @@ -3096,7 +3119,7 @@ current_namespace_on_cfp(const rb_execution_context_t *ec, const rb_control_fram VM_NAMESPACE_ASSERT(lep, "lep should be valid"); VM_NAMESPACE_ASSERT(rb_namespace_available(), "namespace should be available here"); - if (VM_ENV_FRAME_TYPE_P(lep, VM_FRAME_MAGIC_METHOD)) { + if (VM_ENV_FRAME_TYPE_P(lep, VM_FRAME_MAGIC_METHOD) || VM_ENV_FRAME_TYPE_P(lep, VM_FRAME_MAGIC_CFUNC)) { cme = check_method_entry(lep[VM_ENV_DATA_INDEX_ME_CREF], TRUE); VM_NAMESPACE_ASSERT(cme, "cme should be valid"); VM_NAMESPACE_ASSERT(cme->def, "cme->def shold be valid"); diff --git a/vm_dump.c b/vm_dump.c index 0fcef846db8704..2ed1f955b3445e 100644 --- a/vm_dump.c +++ b/vm_dump.c @@ -421,39 +421,42 @@ rb_vmdebug_namespace_env_dump_raw(const rb_execution_context_t *ec, const rb_con // See VM_EP_RUBY_LEP for the original logic const VALUE *ep = current_cfp->ep; const rb_control_frame_t * const eocfp = RUBY_VM_END_CONTROL_FRAME(ec); /* end of control frame pointer */ - const rb_control_frame_t *cfp = NULL, *checkpoint_cfp = current_cfp; + const rb_control_frame_t *cfp = current_cfp, *checkpoint_cfp = current_cfp; kprintf("-- Namespace detection information " "-----------------------------------------\n"); namespace_env_dump_unchecked(ec, ep, checkpoint_cfp, errout); - while (!VM_ENV_LOCAL_P(ep) || VM_ENV_FRAME_TYPE_P(ep, VM_FRAME_MAGIC_CFUNC)) { + if (VM_ENV_FRAME_TYPE_P(ep, VM_FRAME_MAGIC_IFUNC)) { while (!VM_ENV_LOCAL_P(ep)) { ep = VM_ENV_PREV_EP(ep); namespace_env_dump_unchecked(ec, ep, checkpoint_cfp, errout); } - while (VM_ENV_FLAGS(ep, VM_FRAME_FLAG_CFRAME) != 0) { - if (!cfp) { - cfp = vmdebug_search_cf_from_ep(ec, checkpoint_cfp, ep); - } - if (!cfp) { - goto stop; - } - cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp); - if (cfp >= eocfp) { - kprintf("[PREVIOUS CONTROL FRAME IS OUT OF BOUND]\n"); - goto stop; - } - ep = cfp->ep; - namespace_env_dump_unchecked(ec, ep, checkpoint_cfp, errout); - if (!ep) { - goto stop; - } + goto stop; + } + + while (VM_ENV_FRAME_TYPE_P(ep, VM_FRAME_MAGIC_CFUNC)) { + cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp); + if (!cfp) { + goto stop; + } + if (cfp >= eocfp) { + kprintf("[PREVIOUS CONTROL FRAME IS OUT OF BOUND]\n"); + goto stop; + } + ep = cfp->ep; + namespace_env_dump_unchecked(ec, ep, checkpoint_cfp, errout); + if (!ep) { + goto stop; } - checkpoint_cfp = cfp; - cfp = NULL; } + + while (!VM_ENV_LOCAL_P(ep)) { + ep = VM_ENV_PREV_EP(ep); + namespace_env_dump_unchecked(ec, ep, checkpoint_cfp, errout); + } + stop: kprintf("\n"); return true; From 0116dd5e0caaf8136f867b3037ce6d8dbf376232 Mon Sep 17 00:00:00 2001 From: Satoshi Tagomori Date: Sat, 1 Nov 2025 08:03:33 +0900 Subject: [PATCH 12/15] Make Namespace.root visible not only for debugging There are many APIs that expects application codes overwrite global methods. For example, warn() expects Warning.warn() is overwritten to hook warning messages. If we enable namespace, Warning.warn defined in the app code is visible only in the namespace, and invisible from warn() defined in the root namespace. So we have to enable users to overwrite Warning.warn in the root namespace. This is ugly and temporal workaround. We need to define better APIs to enable users to hook such behaviors in the different way from defining global methods. --- namespace.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/namespace.c b/namespace.c index 3498a4cc4ae433..2ae91025455e80 100644 --- a/namespace.c +++ b/namespace.c @@ -1084,14 +1084,13 @@ Init_Namespace(void) if (rb_namespace_available()) { rb_include_module(rb_cObject, rb_mNamespaceLoader); -#if RUBY_DEBUG rb_define_singleton_method(rb_cNamespace, "root", rb_namespace_s_root, 0); rb_define_singleton_method(rb_cNamespace, "main", rb_namespace_s_main, 0); - rb_define_global_function("dump_classext", rb_f_dump_classext, 1); - rb_define_method(rb_cNamespace, "root?", rb_namespace_root_p, 0); rb_define_method(rb_cNamespace, "main?", rb_namespace_main_p, 0); - rb_define_method(rb_cNamespace, "user?", rb_namespace_user_p, 0); + +#if RUBY_DEBUG + rb_define_global_function("dump_classext", rb_f_dump_classext, 1); #endif } From 89fa15b6f69ebb5c9d71aab5bf19d5ac2b67b05b Mon Sep 17 00:00:00 2001 From: Satoshi Tagomori Date: Sat, 1 Nov 2025 09:41:36 +0900 Subject: [PATCH 13/15] Fix incorrect RUBY_DEBUG range --- namespace.c | 44 ++++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/namespace.c b/namespace.c index 2ae91025455e80..0efc75e28bf009 100644 --- a/namespace.c +++ b/namespace.c @@ -876,8 +876,6 @@ Init_enable_namespace(void) } } -#if RUBY_DEBUG > 0 - /* :nodoc: */ static VALUE rb_namespace_s_root(VALUE recv) @@ -892,6 +890,24 @@ rb_namespace_s_main(VALUE recv) return main_namespace->ns_object; } +/* :nodoc: */ +static VALUE +rb_namespace_root_p(VALUE namespace) +{ + const rb_namespace_t *ns = (const rb_namespace_t *)rb_get_namespace_t(namespace); + return RBOOL(NAMESPACE_ROOT_P(ns)); +} + +/* :nodoc: */ +static VALUE +rb_namespace_main_p(VALUE namespace) +{ + const rb_namespace_t *ns = (const rb_namespace_t *)rb_get_namespace_t(namespace); + return RBOOL(NAMESPACE_MAIN_P(ns)); +} + +#if RUBY_DEBUG + static const char * classname(VALUE klass) { @@ -1027,30 +1043,6 @@ rb_f_dump_classext(VALUE recv, VALUE klass) return res; } -/* :nodoc: */ -static VALUE -rb_namespace_root_p(VALUE namespace) -{ - const rb_namespace_t *ns = (const rb_namespace_t *)rb_get_namespace_t(namespace); - return RBOOL(NAMESPACE_ROOT_P(ns)); -} - -/* :nodoc: */ -static VALUE -rb_namespace_main_p(VALUE namespace) -{ - const rb_namespace_t *ns = (const rb_namespace_t *)rb_get_namespace_t(namespace); - return RBOOL(NAMESPACE_MAIN_P(ns)); -} - -/* :nodoc: */ -static VALUE -rb_namespace_user_p(VALUE namespace) -{ - const rb_namespace_t *ns = (const rb_namespace_t *)rb_get_namespace_t(namespace); - return RBOOL(NAMESPACE_USER_P(ns)); -} - #endif /* RUBY_DEBUG */ /* From c83a249f777acf2995a0597127a03967f2b92b5e Mon Sep 17 00:00:00 2001 From: Satoshi Tagomori Date: Sat, 1 Nov 2025 10:43:22 +0900 Subject: [PATCH 14/15] pend on Windows for timeouts --- test/ruby/test_namespace.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/ruby/test_namespace.rb b/test/ruby/test_namespace.rb index 6df960f7305b80..daa4e9e0ba3881 100644 --- a/test/ruby/test_namespace.rb +++ b/test/ruby/test_namespace.rb @@ -758,6 +758,7 @@ def foo_namespace = Namespace.current end def test_loading_extension_libs_in_main_namespace + pend if /mswin|mingw/ =~ RUBY_PLATFORM # timeout on windows environments assert_separately([ENV_ENABLE_NAMESPACE], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) begin; require "prism" From 4a3d8346a6d0e068508631541f6bc43e8b154ea1 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Sun, 2 Nov 2025 17:21:03 +0000 Subject: [PATCH 15/15] [DOC] Tweaks for String#to_f --- string.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/string.c b/string.c index 8b18ad97709712..472159ffe470e1 100644 --- a/string.c +++ b/string.c @@ -7071,7 +7071,7 @@ rb_str_to_i(int argc, VALUE *argv, VALUE str) * '3.14159'.to_f # => 3.14159 * '1.234e-2'.to_f # => 0.01234 * - * Characters past a leading valid number (in the given +base+) are ignored: + * Characters past a leading valid number are ignored: * * '3.14 (pi to two places)'.to_f # => 3.14 * @@ -7079,6 +7079,7 @@ rb_str_to_i(int argc, VALUE *argv, VALUE str) * * 'abcdef'.to_f # => 0.0 * + * See {Converting to Non-String}[rdoc-ref:String@Converting+to+Non--5CString]. */ static VALUE