diff options
author | Étienne Barrié <etienne.barrie@gmail.com> | 2023-12-01 11:33:00 +0100 |
---|---|---|
committer | Jean Boussier <jean.boussier@gmail.com> | 2024-03-19 09:26:49 +0100 |
commit | 12be40ae6be78ac41e8e3f3c313cc6f63e7fa6c4 (patch) | |
tree | f6b81fac770da6b705557623224dbf9b9c2d2847 /spec/ruby | |
parent | 86b15316a748a579dd4fd4df42b6db42accebdc2 (diff) | |
download | ruby-12be40ae6be78ac41e8e3f3c313cc6f63e7fa6c4.tar.gz |
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>
Diffstat (limited to 'spec/ruby')
-rw-r--r-- | spec/ruby/command_line/fixtures/string_literal_frozen_comment.rb | 4 | ||||
-rw-r--r-- | spec/ruby/command_line/fixtures/string_literal_mutable_comment.rb | 4 | ||||
-rw-r--r-- | spec/ruby/command_line/fixtures/string_literal_raw.rb | 3 | ||||
-rw-r--r-- | spec/ruby/command_line/frozen_strings_spec.rb | 35 | ||||
-rw-r--r-- | spec/ruby/core/kernel/eval_spec.rb | 19 | ||||
-rw-r--r-- | spec/ruby/core/string/chilled_string_spec.rb | 69 | ||||
-rw-r--r-- | spec/ruby/language/string_spec.rb | 4 | ||||
-rw-r--r-- | spec/ruby/shared/kernel/object_id.rb | 14 |
8 files changed, 140 insertions, 12 deletions
diff --git a/spec/ruby/command_line/fixtures/string_literal_frozen_comment.rb b/spec/ruby/command_line/fixtures/string_literal_frozen_comment.rb new file mode 100644 index 0000000000..fb84b546c0 --- /dev/null +++ b/spec/ruby/command_line/fixtures/string_literal_frozen_comment.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true +frozen = "test".frozen? +interned = "test".equal?("test") +puts "frozen:#{frozen} interned:#{interned}" diff --git a/spec/ruby/command_line/fixtures/string_literal_mutable_comment.rb b/spec/ruby/command_line/fixtures/string_literal_mutable_comment.rb new file mode 100644 index 0000000000..381a742001 --- /dev/null +++ b/spec/ruby/command_line/fixtures/string_literal_mutable_comment.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: false +frozen = "test".frozen? +interned = "test".equal?("test") +puts "frozen:#{frozen} interned:#{interned}" diff --git a/spec/ruby/command_line/fixtures/string_literal_raw.rb b/spec/ruby/command_line/fixtures/string_literal_raw.rb new file mode 100644 index 0000000000..56b1841296 --- /dev/null +++ b/spec/ruby/command_line/fixtures/string_literal_raw.rb @@ -0,0 +1,3 @@ +frozen = "test".frozen? +interned = "test".equal?("test") +puts "frozen:#{frozen} interned:#{interned}" diff --git a/spec/ruby/command_line/frozen_strings_spec.rb b/spec/ruby/command_line/frozen_strings_spec.rb index 647b69daed..24b979b68b 100644 --- a/spec/ruby/command_line/frozen_strings_spec.rb +++ b/spec/ruby/command_line/frozen_strings_spec.rb @@ -19,6 +19,41 @@ describe "The --enable-frozen-string-literal flag causes string literals to" do end end +describe "The --disable-frozen-string-literal flag causes string literals to" do + + it "produce a different object each time" do + ruby_exe(fixture(__FILE__, "freeze_flag_one_literal.rb"), options: "--disable-frozen-string-literal").chomp.should == "false" + end + +end + +describe "With neither --enable-frozen-string-literal nor --disable-frozen-string-literal flag set" do + + it "produce a different object each time" do + ruby_exe(fixture(__FILE__, "freeze_flag_one_literal.rb")).chomp.should == "false" + end + + ruby_version_is "3.4" do + it "if file has no frozen_string_literal comment produce different frozen strings each time" do + ruby_exe(fixture(__FILE__, "string_literal_raw.rb")).chomp.should == "frozen:true interned:false" + end + end + + ruby_version_is ""..."3.4" do + it "if file has no frozen_string_literal comment produce different mutable strings each time" do + ruby_exe(fixture(__FILE__, "string_literal_raw.rb")).chomp.should == "frozen:false interned:false" + end + end + + it "if file has frozen_string_literal:true comment produce same frozen strings each time" do + ruby_exe(fixture(__FILE__, "string_literal_frozen_comment.rb")).chomp.should == "frozen:true interned:true" + end + + it "if file has frozen_string_literal:false comment produce different mutable strings each time" do + ruby_exe(fixture(__FILE__, "string_literal_mutable_comment.rb")).chomp.should == "frozen:false interned:false" + end +end + describe "The --debug flag produces" do it "debugging info on attempted frozen string modification" do error_str = ruby_exe(fixture(__FILE__, 'debug_info.rb'), options: '--debug', args: "2>&1") diff --git a/spec/ruby/core/kernel/eval_spec.rb b/spec/ruby/core/kernel/eval_spec.rb index 15c9d511fc..5d82f57e44 100644 --- a/spec/ruby/core/kernel/eval_spec.rb +++ b/spec/ruby/core/kernel/eval_spec.rb @@ -350,9 +350,11 @@ CODE end it "allows a magic encoding comment and a subsequent frozen_string_literal magic comment" do + frozen_string_default = "test".frozen? + code = <<CODE.b # encoding: UTF-8 -# frozen_string_literal: true +# frozen_string_literal: #{!frozen_string_default} class EvalSpecs Vπstring = "frozen" end @@ -362,7 +364,7 @@ CODE EvalSpecs.constants(false).should include(:"Vπstring") EvalSpecs::Vπstring.should == "frozen" EvalSpecs::Vπstring.encoding.should == Encoding::UTF_8 - EvalSpecs::Vπstring.frozen?.should be_true + EvalSpecs::Vπstring.frozen?.should == !frozen_string_default end it "allows a magic encoding comment and a frozen_string_literal magic comment on the same line in emacs style" do @@ -381,8 +383,9 @@ CODE end it "ignores the magic encoding comment if it is after a frozen_string_literal magic comment" do + frozen_string_default = "test".frozen? code = <<CODE.b -# frozen_string_literal: true +# frozen_string_literal: #{!frozen_string_default} # encoding: UTF-8 class EvalSpecs Vπfrozen_first = "frozen" @@ -396,24 +399,24 @@ CODE value = EvalSpecs.const_get(binary_constant) value.should == "frozen" value.encoding.should == Encoding::BINARY - value.frozen?.should be_true + value.frozen?.should == !frozen_string_default end it "ignores the frozen_string_literal magic comment if it appears after a token and warns if $VERBOSE is true" do - default_frozen_string_literal = "test".frozen? + frozen_string_default = "test".frozen? code = <<CODE some_token_before_magic_comment = :anything -# frozen_string_literal: true +# frozen_string_literal: #{!frozen_string_default} class EvalSpecs Vπstring_not_frozen = "not frozen" end CODE -> { eval(code) }.should complain(/warning: [`']frozen_string_literal' is ignored after any tokens/, verbose: true) - EvalSpecs::Vπstring_not_frozen.frozen?.should == default_frozen_string_literal + EvalSpecs::Vπstring_not_frozen.frozen?.should == frozen_string_default EvalSpecs.send :remove_const, :Vπstring_not_frozen -> { eval(code) }.should_not complain(verbose: false) - EvalSpecs::Vπstring_not_frozen.frozen?.should == default_frozen_string_literal + EvalSpecs::Vπstring_not_frozen.frozen?.should == frozen_string_default EvalSpecs.send :remove_const, :Vπstring_not_frozen end end diff --git a/spec/ruby/core/string/chilled_string_spec.rb b/spec/ruby/core/string/chilled_string_spec.rb new file mode 100644 index 0000000000..8de4fc421b --- /dev/null +++ b/spec/ruby/core/string/chilled_string_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' + +describe "chilled String" do + guard -> { ruby_version_is "3.4" and !"test".equal?("test") } do + describe "#frozen?" do + it "returns true" do + "chilled".frozen?.should == true + end + end + + describe "#-@" do + it "returns a different instance" do + input = "chilled" + interned = (-input) + interned.frozen?.should == true + interned.object_id.should_not == input.object_id + end + end + + describe "#+@" do + it "returns a different instance" do + input = "chilled" + duped = (+input) + duped.frozen?.should == false + duped.object_id.should_not == input.object_id + end + end + + describe "#clone" do + it "preserves chilled status" do + input = "chilled".clone + -> { + input << "-mutated" + }.should complain(/literal string will be frozen in the future/) + input.should == "chilled-mutated" + end + end + + describe "mutation" do + it "emits a warning" do + input = "chilled" + -> { + input << "-mutated" + }.should complain(/literal string will be frozen in the future/) + input.should == "chilled-mutated" + end + + it "emits a warning on singleton_class creaation" do + -> { + "chilled".singleton_class + }.should complain(/literal string will be frozen in the future/) + end + + it "emits a warning on instance variable assignment" do + -> { + "chilled".instance_variable_set(:@ivar, 42) + }.should complain(/literal string will be frozen in the future/) + end + + it "raises FrozenError after the string was explictly frozen" do + input = "chilled" + input.freeze + -> { + input << "mutated" + }.should raise_error(FrozenError) + end + end + end +end diff --git a/spec/ruby/language/string_spec.rb b/spec/ruby/language/string_spec.rb index f2764eada0..1a1cd35850 100644 --- a/spec/ruby/language/string_spec.rb +++ b/spec/ruby/language/string_spec.rb @@ -232,8 +232,8 @@ describe "Ruby String literals" do end it "produce different objects for literals with the same content in different files if the other file doesn't have the comment" do - frozen_literals_by_default = eval("'test'").frozen? - ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files_no_comment.rb")).chomp.should == (!frozen_literals_by_default).to_s + frozen_string_literal = "test".frozen? && "test".equal?("test") + ruby_exe(fixture(__FILE__, "freeze_magic_comment_across_files_no_comment.rb")).chomp.should == (!frozen_string_literal).to_s end it "produce different objects for literals with the same content in different files if they have different encodings" do diff --git a/spec/ruby/shared/kernel/object_id.rb b/spec/ruby/shared/kernel/object_id.rb index 3e032102f1..099df8ff94 100644 --- a/spec/ruby/shared/kernel/object_id.rb +++ b/spec/ruby/shared/kernel/object_id.rb @@ -52,7 +52,7 @@ describe :object_id, shared: true do o1.send(@method).should_not == o2.send(@method) end - guard -> { "test".frozen? } do # --enable-frozen-string-literal in $RUBYOPT + guard -> { "test".frozen? && "test".equal?("test") } do # --enable-frozen-string-literal in $RUBYOPT it "returns the same value for two identical String literals" do o1 = "hello" o2 = "hello" @@ -60,7 +60,17 @@ describe :object_id, shared: true do end end - guard_not -> { "test".frozen? } do + guard -> { "test".frozen? && !"test".equal?("test") } do # chilled string literals + it "returns a different frozen value for two String literals" do + o1 = "hello" + o2 = "hello" + o1.send(@method).should_not == o2.send(@method) + o1.frozen?.should == true + o2.frozen?.should == true + end + end + + guard -> { !"test".frozen? } do it "returns a different value for two String literals" do o1 = "hello" o2 = "hello" |