From 2c574ca7e29d76f53e45f055f2acf078625e2f7f Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 10 Jul 2026 07:37:36 +0200 Subject: [PATCH] Symbol#to_s now returns a frozen string. [Feature #22137] It has returned a chilled string, hence emitting deprecation warnings for two versions. --- NEWS.md | 5 ++++ error.c | 9 -------- ext/-test-/string/capacity.c | 7 ++++++ include/ruby/internal/intern/error.h | 2 +- internal/string.h | 17 +++----------- spec/ruby/core/string/chilled_string_spec.rb | 2 ++ spec/ruby/core/symbol/to_s_spec.rb | 8 ++++++- string.c | 13 ++++------- symbol.rb | 24 +++++--------------- test/-ext-/string/test_capacity.rb | 4 ++-- tool/timeline/lib/converter_defs.rb | 3 +-- variable.c | 2 +- vm_insnhelper.c | 3 --- yjit/src/cruby_bindings.inc.rs | 2 +- zjit/src/cruby_bindings.inc.rs | 2 +- zjit/src/hir/tests.rs | 2 +- 16 files changed, 42 insertions(+), 63 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0766b03bf19..087fc9ae4e4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -58,6 +58,10 @@ Note: We're only listing outstanding class updates. * A deprecated behavior, `Set#to_set`, `Range#to_set`, and `Enumerable#to_set` accepting arguments, was removed. [[Feature #21390]] +* Symbol + + * `Symbol#to_s` now returns a frozen string. [[Feature #22137]] + ## Stdlib updates ### The following bundled gems are added. @@ -211,6 +215,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #21861]: https://bugs.ruby-lang.org/issues/21861 [Feature #21932]: https://bugs.ruby-lang.org/issues/21932 [Feature #21981]: https://bugs.ruby-lang.org/issues/21981 +[Feature #22137]: https://bugs.ruby-lang.org/issues/22137 [Feature #22139]: https://bugs.ruby-lang.org/issues/22139 [PR #17201]: https://github.com/ruby/ruby/pull/17201 [RubyGems-v4.0.4]: https://github.com/rubygems/rubygems/releases/tag/v4.0.4 diff --git a/error.c b/error.c index 379296c1182..4515590845f 100644 --- a/error.c +++ b/error.c @@ -4271,15 +4271,6 @@ rb_warn_unchilled_literal(VALUE obj) } } -void -rb_warn_unchilled_symbol_to_s(VALUE obj) -{ - rb_category_warn( - RB_WARN_CATEGORY_DEPRECATED, - "string returned by :%s.to_s will be frozen in the future", RSTRING_PTR(obj) - ); -} - #undef rb_check_frozen void rb_check_frozen(VALUE obj) diff --git a/ext/-test-/string/capacity.c b/ext/-test-/string/capacity.c index 33b2023fd3b..26eb6f4ff14 100644 --- a/ext/-test-/string/capacity.c +++ b/ext/-test-/string/capacity.c @@ -11,8 +11,15 @@ bug_str_capacity(VALUE klass, VALUE str) return LONG2FIX(rb_str_capacity(str)); } +static VALUE +bug_str_new_shared(VALUE klass, VALUE str) +{ + return rb_str_new_shared(str); +} + void Init_string_capacity(VALUE klass) { rb_define_singleton_method(klass, "capacity", bug_str_capacity, 1); + rb_define_singleton_method(klass, "rb_str_new_shared", bug_str_new_shared, 1); } diff --git a/include/ruby/internal/intern/error.h b/include/ruby/internal/intern/error.h index 1fd9ec2f516..ca6664fcc18 100644 --- a/include/ruby/internal/intern/error.h +++ b/include/ruby/internal/intern/error.h @@ -258,7 +258,7 @@ rb_check_frozen_inline(VALUE obj) /* ref: internal CHILLED_STRING_P() This is an implementation detail subject to change. */ - if (RB_UNLIKELY(RB_TYPE_P(obj, T_STRING) && FL_TEST_RAW(obj, RUBY_FL_USER2 | RUBY_FL_USER3))) { // STR_CHILLED + if (RB_UNLIKELY(RB_TYPE_P(obj, T_STRING) && FL_TEST_RAW(obj, RUBY_FL_USER2))) { // STR_CHILLED rb_str_modify(obj); } } diff --git a/internal/string.h b/internal/string.h index 02d20a46262..060703e2856 100644 --- a/internal/string.h +++ b/internal/string.h @@ -18,9 +18,7 @@ #define STR_SHARED FL_USER0 /* = ELTS_SHARED */ #define STR_NOEMBED FL_USER1 -#define STR_CHILLED (FL_USER2 | FL_USER3) -#define STR_CHILLED_LITERAL FL_USER2 -#define STR_CHILLED_SYMBOL_TO_S FL_USER3 +#define STR_CHILLED FL_USER2 #define STR_FAKESTR FL_USER19 enum ruby_rstring_private_flags { @@ -103,7 +101,6 @@ VALUE rb_str_casecmp(VALUE str1, VALUE str2); /* error.c */ void rb_warn_unchilled_literal(VALUE str); -void rb_warn_unchilled_symbol_to_s(VALUE str); static inline bool STR_EMBED_P(VALUE str); static inline bool STR_SHARED_P(VALUE str); @@ -173,17 +170,9 @@ CHILLED_STRING_P(VALUE obj) static inline void CHILLED_STRING_MUTATED(VALUE str) { - VALUE chilled_reason = RB_FL_TEST_RAW(str, STR_CHILLED); - FL_UNSET_RAW(str, STR_CHILLED); - switch (chilled_reason) { - case STR_CHILLED_SYMBOL_TO_S: - rb_warn_unchilled_symbol_to_s(str); - break; - case STR_CHILLED_LITERAL: + if (RB_FL_TEST_RAW(str, STR_CHILLED)) { + FL_UNSET_RAW(str, STR_CHILLED); rb_warn_unchilled_literal(str); - break; - default: - rb_bug("RString was chilled for multiple reasons"); } } diff --git a/spec/ruby/core/string/chilled_string_spec.rb b/spec/ruby/core/string/chilled_string_spec.rb index 6a5e846db26..452b937185e 100644 --- a/spec/ruby/core/string/chilled_string_spec.rb +++ b/spec/ruby/core/string/chilled_string_spec.rb @@ -78,7 +78,9 @@ describe "chilled String" do end end end + end + guard -> { ruby_version_is "3.4"..."4.1" and !:test.to_s.equal?(:test.to_s) } do describe "chilled strings returned by Symbol#to_s" do describe "#frozen?" do diff --git a/spec/ruby/core/symbol/to_s_spec.rb b/spec/ruby/core/symbol/to_s_spec.rb index 2cb57c4cbcf..6d79f946bde 100644 --- a/spec/ruby/core/symbol/to_s_spec.rb +++ b/spec/ruby/core/symbol/to_s_spec.rb @@ -16,7 +16,7 @@ describe "Symbol#to_s" do symbol.to_s.encoding.should == Encoding::US_ASCII end - ruby_version_is "3.4" do + ruby_version_is "3.4"..."4.1" do it "warns about mutating returned string" do -> { :bad!.to_s.upcase! }.should complain(/warning: string returned by :bad!.to_s will be frozen in the future/) end @@ -29,4 +29,10 @@ describe "Symbol#to_s" do Warning[:deprecated] = deprecated end end + + ruby_version_is "4.1" do + it "returns a frozen string" do + :test.to_s.frozen?.should == true + end + end end diff --git a/string.c b/string.c index 8d82cdee823..322e43eb2fd 100644 --- a/string.c +++ b/string.c @@ -90,12 +90,9 @@ VALUE rb_cSymbol; * The string is not embedded. When a string is embedded, the contents * follow the header. When a string is not embedded, the contents is * on a separately allocated buffer. - * 2: STR_CHILLED_LITERAL (will be frozen in a future version) + * 2: STR_CHILLED (will be frozen in a future version) * The string was allocated as a literal in a file without an explicit `frozen_string_literal` comment. * It emits a deprecation warning when mutated for the first time. - * 3: STR_CHILLED_SYMBOL_TO_S (will be frozen in a future version) - * The string was allocated by the `Symbol#to_s` method. - * It emits a deprecation warning when mutated for the first time. * 4: STR_PRECOMPUTED_HASH * The string is embedded and has its precomputed hashcode stored * after the terminator. @@ -2035,7 +2032,7 @@ rb_ec_str_resurrect(struct rb_execution_context_struct *ec, VALUE str, bool chil str_duplicate_setup_heap(klass, str, new_str); } if (chilled) { - FL_SET_RAW(new_str, STR_CHILLED_LITERAL); + FL_SET_RAW(new_str, STR_CHILLED); } return new_str; } @@ -2046,7 +2043,7 @@ rb_str_with_debug_created_info(VALUE str, VALUE path, int line) VALUE debug_info = rb_ary_new_from_args(2, path, INT2FIX(line)); if (OBJ_FROZEN_RAW(str)) str = rb_str_dup(str); rb_ivar_set(str, id_debug_created_info, rb_ary_freeze(debug_info)); - FL_SET_RAW(str, STR_CHILLED_LITERAL); + FL_SET_RAW(str, STR_CHILLED); return rb_str_freeze(str); } @@ -12441,9 +12438,7 @@ sym_inspect(VALUE sym) VALUE rb_sym_to_s(VALUE sym) { - VALUE str = str_new_shared(rb_cString, rb_sym2str(sym)); - FL_SET_RAW(str, STR_CHILLED_SYMBOL_TO_S); - return str; + return rb_sym2str(sym); } VALUE diff --git a/symbol.rb b/symbol.rb index 089e91f63bf..05fd06bb85d 100644 --- a/symbol.rb +++ b/symbol.rb @@ -2,32 +2,20 @@ class Symbol # call-seq: # to_s -> string # - # Returns a string representation of +self+ (not including the leading colon): - # - # :foo.to_s # => "foo" - # - # Related: Symbol#inspect, Symbol#name. - def to_s - Primitive.attr! :leaf - Primitive.cexpr! 'rb_sym_to_s(self)' - end - - alias id2name to_s - - # call-seq: - # name -> string - # # Returns a frozen string representation of +self+ (not including the leading colon): # - # :foo.name # => "foo" + # :foo.to_s # => "foo" # :foo.name.frozen? # => true # - # Related: Symbol#to_s, Symbol#inspect. - def name + # Related: Symbol#inspect + def to_s Primitive.attr! :leaf Primitive.cexpr! 'rb_sym2str(self)' end + alias id2name to_s + alias name to_s + # call-seq: # empty? -> true or false # diff --git a/test/-ext-/string/test_capacity.rb b/test/-ext-/string/test_capacity.rb index 3766dd3d303..ca7ba23ce26 100644 --- a/test/-ext-/string/test_capacity.rb +++ b/test/-ext-/string/test_capacity.rb @@ -12,8 +12,8 @@ class Test_StringCapacity < Test::Unit::TestCase end def test_capacity_shared - sym = ("a" * pool_slot_size(0)).to_sym - assert_equal 0, capa(sym.to_s) + source = ("a" * pool_slot_size(0)).freeze + assert_equal 0, capa(Bug::String.rb_str_new_shared(source)) end def test_capacity_normal diff --git a/tool/timeline/lib/converter_defs.rb b/tool/timeline/lib/converter_defs.rb index eeb61c9aee5..e4cd2bc079b 100644 --- a/tool/timeline/lib/converter_defs.rb +++ b/tool/timeline/lib/converter_defs.rb @@ -91,8 +91,7 @@ module RubyTimelineTool StringFlags = FlagsConverter.new({ STR_SHARED: FL_USER_N(0), RSTRING_NOEMBED: FL_USER_N(1), - STR_CHILLED_LITERAL: FL_USER_N(2), - STR_CHILLED_SYMBOL_TO_S: FL_USER_N(3), + STR_CHILLED: FL_USER_N(2), RSTRING_FSTR: FL_USER_N(17), }) diff --git a/variable.c b/variable.c index 55e9e2f9335..994feb7a202 100644 --- a/variable.c +++ b/variable.c @@ -2008,7 +2008,7 @@ rb_obj_freeze_inline(VALUE x) if (RB_FL_ABLE(x)) { RB_FL_SET_RAW(x, RUBY_FL_FREEZE); if (TYPE(x) == T_STRING) { - RB_FL_UNSET_RAW(x, FL_USER2 | FL_USER3); // STR_CHILLED + RB_FL_UNSET_RAW(x, FL_USER2); // STR_CHILLED } // rb_obj_freeze_inline(String) diff --git a/vm_insnhelper.c b/vm_insnhelper.c index e6f23f27beb..06ad9769152 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -6188,9 +6188,6 @@ vm_objtostring(struct rb_control_frame_struct *reg_cfp, VALUE recv, CALL_DATA cd switch (type) { case T_SYMBOL: if (check_method_basic_definition(cme)) { - // rb_sym_to_s() allocates a mutable string, but since we are only - // going to use this string for interpolation, it's fine to use the - // frozen string. return rb_sym2str(recv); } break; diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 8ec0c495bbe..2a9646dfed2 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -679,7 +679,7 @@ pub struct rb_call_data { pub ci: *const rb_callinfo, pub cc: *const rb_callcache, } -pub const RSTRING_CHILLED: ruby_rstring_private_flags = 49152; +pub const RSTRING_CHILLED: ruby_rstring_private_flags = 16384; pub type ruby_rstring_private_flags = u32; pub const RHASH_PASS_AS_KEYWORDS: ruby_rhash_flags = 8192; pub const RHASH_PROC_DEFAULT: ruby_rhash_flags = 16384; diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 6b2c6c0e5f0..ff254f433ec 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1577,7 +1577,7 @@ pub struct rb_call_data { pub ci: *const rb_callinfo, pub cc: *const rb_callcache, } -pub const RSTRING_CHILLED: ruby_rstring_private_flags = 49152; +pub const RSTRING_CHILLED: ruby_rstring_private_flags = 16384; pub type ruby_rstring_private_flags = u32; pub const RHASH_PASS_AS_KEYWORDS: ruby_rhash_flags = 8192; pub const RHASH_PROC_DEFAULT: ruby_rhash_flags = 16384; diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index e3271833326..574cf60b1a1 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -5230,7 +5230,7 @@ pub(crate) mod hir_build_tests { let iseq = crate::cruby::with_rubyvm(|| get_instance_method_iseq("Symbol", "name")); let function = iseq_to_hir(iseq).unwrap(); assert_snapshot!(hir_string_function(&function), @" - fn name@: + fn to_s@: bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf