Skip to content

Commit 12be40a

Browse files
etiennebarriebyroot
andcommitted
Implement chilled strings
[Feature #20205] As a path toward enabling frozen string literals by default in the future, this commit introduce "chilled strings". From a user perspective chilled strings pretend to be frozen, but on the first attempt to mutate them, they lose their frozen status and emit a warning rather than to raise a `FrozenError`. Implementation wise, `rb_compile_option_struct.frozen_string_literal` is no longer a boolean but a tri-state of `enabled/disabled/unset`. When code is compiled with frozen string literals neither explictly enabled or disabled, string literals are compiled with a new `putchilledstring` instruction. This instruction is identical to `putstring` except it marks the String with the `STR_CHILLED (FL_USER3)` and `FL_FREEZE` flags. Chilled strings have the `FL_FREEZE` flag as to minimize the need to check for chilled strings across the codebase, and to improve compatibility with C extensions. Notes: - `String#freeze`: clears the chilled flag. - `String#-@`: acts as if the string was mutable. - `String#+@`: acts as if the string was mutable. - `String#clone`: copies the chilled flag. Co-authored-by: Jean Boussier <byroot@ruby-lang.org>
1 parent 86b1531 commit 12be40a

36 files changed

+714
-282
lines changed

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ Note that each entry is kept to a minimum, see links for details.
77

88
## Language changes
99

10+
* String literals in files without a `frozen_string_literal` comment now behave
11+
as if they were frozen. If they are mutated a deprecation warning is emited.
12+
These warnings can be enabled with `-W:deprecated` or by setting `Warning[:deprecated] = true`.
13+
To disable this change you can run Ruby with the `--disable-frozen-string-literal` command line
14+
argument. [Feature #20205]
15+
1016
* `it` is added to reference a block parameter. [[Feature #18980]]
1117

1218
* Keyword splatting `nil` when calling methods is now supported.

bootstraptest/test_ractor.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,3 +1792,14 @@ class C8; def self.foo = 17; end
17921792
}
17931793

17941794
end # if !ENV['GITHUB_WORKFLOW']
1795+
1796+
# Chilled strings are not shareable
1797+
assert_equal 'false', %q{
1798+
Ractor.shareable?("chilled")
1799+
}
1800+
1801+
# Chilled strings can be made shareable
1802+
assert_equal 'true', %q{
1803+
shareable = Ractor.make_shareable("chilled")
1804+
shareable == "chilled" && Ractor.shareable?(shareable)
1805+
}

bootstraptest/test_yjit.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4679,6 +4679,37 @@ def test(klass, args)
46794679
test(KwInit, [Hash.ruby2_keywords_hash({1 => 1})])
46804680
}
46814681

4682+
# Chilled string setivar trigger warning
4683+
assert_equal 'literal string will be frozen in the future', %q{
4684+
Warning[:deprecated] = true
4685+
$VERBOSE = true
4686+
$warning = "no-warning"
4687+
module ::Warning
4688+
def self.warn(message)
4689+
$warning = message.split("warning: ").last.strip
4690+
end
4691+
end
4692+
4693+
class String
4694+
def setivar!
4695+
@ivar = 42
4696+
end
4697+
end
4698+
4699+
def setivar!(str)
4700+
str.setivar!
4701+
end
4702+
4703+
10.times { setivar!("mutable".dup) }
4704+
10.times do
4705+
setivar!("frozen".freeze)
4706+
rescue FrozenError
4707+
end
4708+
4709+
setivar!("chilled") # Emit warning
4710+
$warning
4711+
}
4712+
46824713
# arity=-2 cfuncs
46834714
assert_equal '["", "1/2", [0, [:ok, 1]]]', %q{
46844715
def test_cases(file, chain)

class.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2244,7 +2244,10 @@ singleton_class_of(VALUE obj)
22442244
return klass;
22452245

22462246
case T_STRING:
2247-
if (FL_TEST_RAW(obj, RSTRING_FSTR)) {
2247+
if (CHILLED_STRING_P(obj)) {
2248+
CHILLED_STRING_MUTATED(obj);
2249+
}
2250+
else if (FL_TEST_RAW(obj, RSTRING_FSTR)) {
22482251
rb_raise(rb_eTypeError, "can't define singleton");
22492252
}
22502253
}

compile.c

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4723,7 +4723,7 @@ frozen_string_literal_p(const rb_iseq_t *iseq)
47234723
return ISEQ_COMPILE_DATA(iseq)->option->frozen_string_literal > 0;
47244724
}
47254725

4726-
static inline int
4726+
static inline bool
47274727
static_literal_node_p(const NODE *node, const rb_iseq_t *iseq, bool hash_key)
47284728
{
47294729
switch (nd_type(node)) {
@@ -10365,12 +10365,18 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const no
1036510365
debugp_param("nd_lit", get_string_value(node));
1036610366
if (!popped) {
1036710367
VALUE lit = get_string_value(node);
10368-
if (!frozen_string_literal_p(iseq)) {
10368+
switch (ISEQ_COMPILE_DATA(iseq)->option->frozen_string_literal) {
10369+
case ISEQ_FROZEN_STRING_LITERAL_UNSET:
10370+
lit = rb_fstring(lit);
10371+
ADD_INSN1(ret, node, putchilledstring, lit);
10372+
RB_OBJ_WRITTEN(iseq, Qundef, lit);
10373+
break;
10374+
case ISEQ_FROZEN_STRING_LITERAL_DISABLED:
1036910375
lit = rb_fstring(lit);
1037010376
ADD_INSN1(ret, node, putstring, lit);
1037110377
RB_OBJ_WRITTEN(iseq, Qundef, lit);
10372-
}
10373-
else {
10378+
break;
10379+
case ISEQ_FROZEN_STRING_LITERAL_ENABLED:
1037410380
if (ISEQ_COMPILE_DATA(iseq)->option->debug_frozen_string_literal || RTEST(ruby_debug)) {
1037510381
VALUE debug_info = rb_ary_new_from_args(2, rb_iseq_path(iseq), INT2FIX(line));
1037610382
lit = rb_str_dup(lit);
@@ -10382,6 +10388,9 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const no
1038210388
}
1038310389
ADD_INSN1(ret, node, putobject, lit);
1038410390
RB_OBJ_WRITTEN(iseq, Qundef, lit);
10391+
break;
10392+
default:
10393+
rb_bug("invalid frozen_string_literal");
1038510394
}
1038610395
}
1038710396
break;

error.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3860,6 +3860,12 @@ void
38603860
rb_error_frozen_object(VALUE frozen_obj)
38613861
{
38623862
rb_yjit_lazy_push_frame(GET_EC()->cfp->pc);
3863+
3864+
if (CHILLED_STRING_P(frozen_obj)) {
3865+
CHILLED_STRING_MUTATED(frozen_obj);
3866+
return;
3867+
}
3868+
38633869
VALUE debug_info;
38643870
const ID created_info = id_debug_created_info;
38653871
VALUE mesg = rb_sprintf("can't modify frozen %"PRIsVALUE": ",

ext/objspace/objspace_dump.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,8 @@ dump_object(VALUE obj, struct dump_config *dc)
476476
dump_append(dc, ", \"embedded\":true");
477477
if (FL_TEST(obj, RSTRING_FSTR))
478478
dump_append(dc, ", \"fstring\":true");
479+
if (CHILLED_STRING_P(obj))
480+
dump_append(dc, ", \"chilled\":true");
479481
if (STR_SHARED_P(obj))
480482
dump_append(dc, ", \"shared\":true");
481483
else

gems/bundled_gems

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55
# - repository-url: URL from where clone for test
66
# - revision: revision in repository-url to test
77
# if `revision` is not given, "v"+`version` or `version` will be used.
8-
minitest 5.22.3 https://github.com/minitest/minitest
9-
power_assert 2.0.3 https://github.com/ruby/power_assert
8+
9+
# Waiting for https://github.com/minitest/minitest/pull/991
10+
minitest 5.22.3 https://github.com/Shopify/minitest b5f5202575894796e00109a8f8a5041b778991ee
11+
12+
# Waiting for https://github.com/ruby/power_assert/pull/48
13+
power_assert 2.0.3 https://github.com/ruby/power_assert 78dd2ab3ccd93796d83c0b35b978c39bfabb938c
1014
rake 13.1.0 https://github.com/ruby/rake
1115
test-unit 3.6.2 https://github.com/test-unit/test-unit
1216
rexml 3.2.6 https://github.com/ruby/rexml
@@ -17,8 +21,8 @@ net-pop 0.1.2 https://github.com/ruby/net-pop
1721
net-smtp 0.4.0.1 https://github.com/ruby/net-smtp
1822
matrix 0.4.2 https://github.com/ruby/matrix
1923
prime 0.1.2 https://github.com/ruby/prime
20-
rbs 3.4.4 https://github.com/ruby/rbs 61b412bc7ba00519e7d6d08450bd384990d94ea2
21-
typeprof 0.21.11 https://github.com/ruby/typeprof
24+
rbs 3.4.4 https://github.com/ruby/rbs ba7872795d5de04adb8ff500c0e6afdc81a041dd
25+
typeprof 0.21.11 https://github.com/ruby/typeprof b19a6416da3a05d57fadd6ffdadb382b6d236ca5
2226
debug 1.9.1 https://github.com/ruby/debug 2d602636d99114d55a32fedd652c9c704446a749
2327
racc 1.7.3 https://github.com/ruby/racc
2428
mutex_m 0.2.0 https://github.com/ruby/mutex_m

include/ruby/internal/fl_type.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,9 @@ static inline void
916916
RB_OBJ_FREEZE_RAW(VALUE obj)
917917
{
918918
RB_FL_SET_RAW(obj, RUBY_FL_FREEZE);
919+
if (TYPE(obj) == T_STRING) {
920+
RB_FL_UNSET_RAW(obj, FL_USER3); // STR_CHILLED
921+
}
919922
}
920923

921924
RUBY_SYMBOL_EXPORT_BEGIN

include/ruby/internal/intern/error.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,6 @@ RBIMPL_ATTR_NONNULL(())
190190
*/
191191
void rb_error_frozen(const char *what);
192192

193-
RBIMPL_ATTR_NORETURN()
194193
/**
195194
* Identical to rb_error_frozen(), except it takes arbitrary Ruby object
196195
* instead of C's string.

0 commit comments

Comments
 (0)