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 /test/ruby | |
| parent | 86b15316a748a579dd4fd4df42b6db42accebdc2 (diff) | |
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 'test/ruby')
| -rw-r--r-- | test/ruby/test_compile_prism.rb | 65 | ||||
| -rw-r--r-- | test/ruby/test_string.rb | 33 |
2 files changed, 81 insertions, 17 deletions
diff --git a/test/ruby/test_compile_prism.rb b/test/ruby/test_compile_prism.rb index 8af9f1ade0..eef909eb07 100644 --- a/test/ruby/test_compile_prism.rb +++ b/test/ruby/test_compile_prism.rb @@ -592,22 +592,16 @@ module Prism assert_prism_eval('$pit = 1; "1 #$pit 1"') assert_prism_eval('"1 #{1 + 2} 1"') assert_prism_eval('"Prism" "::" "TestCompilePrism"') - assert_prism_eval('("a""b").frozen?') - assert_prism_eval(<<-CODE) - # frozen_string_literal: true - - ("a""b").frozen? - CODE - assert_prism_eval(<<-CODE) + assert_prism_eval(<<-'RUBY') # frozen_string_literal: true - ("a""b""#{1}").frozen? - CODE - assert_prism_eval(<<-CODE) + !("a""b""#{1}").frozen? + RUBY + assert_prism_eval(<<-'RUBY') # frozen_string_literal: true - ("a""#{1}""b").frozen? - CODE + !("a""#{1}""b").frozen? + RUBY # Test encoding of interpolated strings assert_prism_eval(<<~'RUBY') @@ -620,6 +614,15 @@ module Prism RUBY end + def test_concatenated_StringNode + assert_prism_eval('("a""b").frozen?') + assert_prism_eval(<<-CODE) + # frozen_string_literal: true + + ("a""b").frozen? + CODE + end + def test_InterpolatedSymbolNode assert_prism_eval('$pit = 1; :"1 #$pit 1"') assert_prism_eval(':"1 #{1 + 2} 1"') @@ -673,7 +676,9 @@ module Prism def test_StringNode assert_prism_eval('"pit"') assert_prism_eval('"a".frozen?') + end + def test_StringNode_frozen_string_literal_true [ # Test that string literal is frozen <<~RUBY, @@ -690,6 +695,31 @@ module Prism end end + def test_StringNode_frozen_string_literal_false + [ + # Test that string literal is frozen + <<~RUBY, + # frozen_string_literal: false + !"a".frozen? + RUBY + # Test that two string literals with the same contents are the same string + <<~RUBY, + # frozen_string_literal: false + !"hello".equal?("hello") + RUBY + ].each do |src| + assert_prism_eval(src, raw: true) + end + end + + def test_StringNode_frozen_string_literal_default + # Test that string literal is chilled + assert_prism_eval('"a".frozen?') + + # Test that two identical chilled string literals aren't the same object + assert_prism_eval('!"hello".equal?("hello")') + end + def test_SymbolNode assert_prism_eval(":pit") @@ -2620,27 +2650,28 @@ end private - def compare_eval(source, raw:) + def compare_eval(source, raw:, location:) source = raw ? source : "class Prism::TestCompilePrism\n#{source}\nend" ruby_eval = RubyVM::InstructionSequence.compile(source).eval prism_eval = RubyVM::InstructionSequence.compile_prism(source).eval if ruby_eval.is_a? Proc - assert_equal ruby_eval.class, prism_eval.class + assert_equal ruby_eval.class, prism_eval.class, "@#{location.path}:#{location.lineno}" else - assert_equal ruby_eval, prism_eval + assert_equal ruby_eval, prism_eval, "@#{location.path}:#{location.lineno}" end end def assert_prism_eval(source, raw: false) + location = caller_locations(1, 1).first $VERBOSE, verbose_bak = nil, $VERBOSE begin - compare_eval(source, raw:) + compare_eval(source, raw:, location:) # Test "popped" functionality - compare_eval("#{source}; 1", raw:) + compare_eval("#{source}; 1", raw:, location:) ensure $VERBOSE = verbose_bak end diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index 58e1af2b61..3f1b91a6a9 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -3610,6 +3610,39 @@ CODE assert_bytesplice_raise(ArgumentError, S("hello"), 0..-1, "bye", 0, 3) end + def test_chilled_string + chilled_string = eval('"chilled"') + + # Chilled strings pretend to be frozen + assert_predicate chilled_string, :frozen? + + assert_not_predicate chilled_string.dup, :frozen? + assert_predicate chilled_string.clone, :frozen? + + # @+ treat the original string as frozen + assert_not_predicate +chilled_string, :frozen? + assert_not_same chilled_string, +chilled_string + + # @- the the original string as mutable + assert_predicate -chilled_string, :frozen? + assert_not_same chilled_string, -chilled_string + end + + def test_chilled_string_setivar + String.class_eval <<~RUBY, __FILE__, __LINE__ + 1 + def setivar! + @ivar = 42 + @ivar + end + RUBY + chilled_string = eval('"chilled"') + begin + assert_equal 42, chilled_string.setivar! + ensure + String.undef_method(:setivar!) + end + end + private def assert_bytesplice_result(expected, s, *args) |
