Symbol#to_s now returns a frozen string.

[Feature #22137]

It has returned a chilled string, hence emitting deprecation
warnings for two versions.
This commit is contained in:
Jean Boussier authored and Jean Boussier committed 2026-07-10 10:02:00 +02:00
1 parent 3782b63b79
commit 2c574ca7e2
16 files changed
+42 -63
Notes: git 2026-07-10 08:02:32 +00:00

No files matched your search

+5
View File
@@ -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
-9
View File
@@ -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)
+7
View File
@@ -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);
}
+1 -1
View File
@@ -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);
}
}
+3 -14
View File
@@ -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");
}
}
@@ -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
+7 -1
View File
@@ -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
+4 -9
View File
@@ -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
+6 -18
View File
@@ -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
#
+2 -2
View File
@@ -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
+1 -2
View File
@@ -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),
})
+1 -1
View File
@@ -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)
-3
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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@<internal:symbol>:
fn to_s@<internal:symbol>:
bb1():
EntryPoint interpreter
v1:BasicObject = LoadSelf