summaryrefslogtreecommitdiff
path: root/test/ruby
diff options
context:
space:
mode:
authorÉtienne Barrié <etienne.barrie@gmail.com>2023-12-01 11:33:00 +0100
committerJean Boussier <jean.boussier@gmail.com>2024-03-19 09:26:49 +0100
commit12be40ae6be78ac41e8e3f3c313cc6f63e7fa6c4 (patch)
treef6b81fac770da6b705557623224dbf9b9c2d2847 /test/ruby
parent86b15316a748a579dd4fd4df42b6db42accebdc2 (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.rb65
-rw-r--r--test/ruby/test_string.rb33
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)