diff --git a/NEWS.md b/NEWS.md index 53d4488dd8e296..a2a1da19441609 100644 --- a/NEWS.md +++ b/NEWS.md @@ -140,7 +140,7 @@ They are still available on rubygems.org and can be installed with * 4.0.3 to [v4.0.4][RubyGems-v4.0.4], [v4.0.5][RubyGems-v4.0.5], [v4.0.6][RubyGems-v4.0.6], [v4.0.7][RubyGems-v4.0.7], [v4.0.8][RubyGems-v4.0.8], [v4.0.9][RubyGems-v4.0.9], [v4.0.10][RubyGems-v4.0.10], [v4.0.11][RubyGems-v4.0.11], [v4.0.12][RubyGems-v4.0.12], [v4.0.13][RubyGems-v4.0.13], [v4.0.14][RubyGems-v4.0.14], [v4.0.15][RubyGems-v4.0.15], [v4.0.16][RubyGems-v4.0.16], [v4.0.17][RubyGems-v4.0.17] * bundler 4.1.0.dev * 4.0.3 to [v4.0.4][bundler-v4.0.4], [v4.0.5][bundler-v4.0.5], [v4.0.6][bundler-v4.0.6], [v4.0.7][bundler-v4.0.7], [v4.0.8][bundler-v4.0.8], [v4.0.9][bundler-v4.0.9], [v4.0.10][bundler-v4.0.10], [v4.0.11][bundler-v4.0.11], [v4.0.12][bundler-v4.0.12], [v4.0.13][bundler-v4.0.13], [v4.0.14][bundler-v4.0.14], [v4.0.15][bundler-v4.0.15], [v4.0.16][bundler-v4.0.16], [v4.0.17][bundler-v4.0.17] -* erb 6.0.6 +* erb 6.0.7 * 6.0.1 to [v6.0.1.1][erb-v6.0.1.1], [v6.0.2][erb-v6.0.2], [v6.0.3][erb-v6.0.3], [v6.0.4][erb-v6.0.4], [v6.0.5][erb-v6.0.5], [v6.0.6][erb-v6.0.6] * error_highlight 0.7.2 * ipaddr 1.2.9 diff --git a/internal/string.h b/internal/string.h index 6dd6c2e4dd231e..a8893a42f1e10a 100644 --- a/internal/string.h +++ b/internal/string.h @@ -93,6 +93,7 @@ void rb_str_make_embedded(VALUE); VALUE rb_str_upto_each(VALUE, VALUE, int, int (*each)(VALUE, VALUE), VALUE); size_t rb_str_size_as_embedded(VALUE); bool rb_str_reembeddable_p(VALUE); +bool rb_str_embedded_shared_root_p(VALUE); VALUE rb_str_upto_endless_each(VALUE, int (*each)(VALUE, VALUE), VALUE); VALUE rb_str_with_debug_created_info(VALUE, VALUE, int); VALUE rb_str_frozen_bare_string(VALUE); diff --git a/lib/erb.rb b/lib/erb.rb index 0c4653d291f28d..040cd183528b05 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -815,9 +815,6 @@ # [template processor]: https://en.wikipedia.org/wiki/Template_processor # class ERB - IDENTITY_METHOD = BasicObject.instance_method(:equal?) # :nodoc: - private_constant :IDENTITY_METHOD - # :markup: markdown # # :call-seq: @@ -1117,7 +1114,7 @@ def new_toplevel(vars = nil) private :new_toplevel def initialized_by_new? # :nodoc: - IDENTITY_METHOD.bind_call(@_init, self.class.singleton_class) + self.class.singleton_class.equal? @_init end private :initialized_by_new? diff --git a/lib/erb/compiler.rb b/lib/erb/compiler.rb index 6d70288b4f55c1..b6185cdc4c84fe 100644 --- a/lib/erb/compiler.rb +++ b/lib/erb/compiler.rb @@ -80,16 +80,11 @@ def initialize(str) end class Scanner # :nodoc: - @scanner_map = defined?(Ractor) ? Ractor.make_shareable({}) : {} + @scanner_map = {}.freeze + class << self - if defined?(Ractor) - def register_scanner(klass, trim_mode, percent) - @scanner_map = Ractor.make_shareable({ **@scanner_map, [trim_mode, percent] => klass }) - end - else - def register_scanner(klass, trim_mode, percent) - @scanner_map[[trim_mode, percent]] = klass - end + def register_scanner(klass, trim_mode, percent) + @scanner_map = @scanner_map.merge([trim_mode, percent].freeze => klass).freeze end alias :regist_scanner :register_scanner end diff --git a/lib/erb/version.rb b/lib/erb/version.rb index 04908528c99d69..2ae8041d2362f1 100644 --- a/lib/erb/version.rb +++ b/lib/erb/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class ERB # The string \ERB version. - VERSION = '6.0.6' + VERSION = '6.0.7' end diff --git a/ractor.c b/ractor.c index fbf39478ffb771..4e741f000df0b6 100644 --- a/ractor.c +++ b/ractor.c @@ -17,6 +17,7 @@ #include "internal/rational.h" #include "internal/struct.h" #include "internal/st.h" +#include "internal/string.h" #include "internal/thread.h" #include "variable.h" #include "yjit.h" @@ -2124,6 +2125,11 @@ move_leave(VALUE obj, struct obj_traverse_replace_data *data) VALUE flags = T_OBJECT | FL_FREEZE | (RBASIC(obj)->flags & FL_PROMOTED); shape_id_t shape_id = (RBASIC_SHAPE_ID(obj) & SHAPE_ID_CAPACITY_MASK) | ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_ROBJECT | SHAPE_ID_FL_FROZEN; + // A copy-on-write sharer reads its bytes straight out of an embedded root's slot + // (String#dup of a frozen string), and it outlives the move, so that body has to + // survive as it is. + bool wipe_body = !(RB_TYPE_P(obj, T_STRING) && rb_str_embedded_shared_root_p(obj)); + // Avoid mutations using bind_call, etc. size_t slot_size = rb_gc_obj_slot_size(obj); MEMZERO((char *)obj, char, sizeof(struct RBasic)); @@ -2134,7 +2140,9 @@ move_leave(VALUE obj, struct obj_traverse_replace_data *data) // but C code that held the object from before the move still reads it with its // old type (an Array iteration in progress, the RMatch capa behind $~): a zeroed // body makes those reads see an empty object instead of stale internals. - MEMZERO((char *)obj + sizeof(struct RBasic), char, slot_size - sizeof(struct RBasic)); + if (wipe_body) { + MEMZERO((char *)obj + sizeof(struct RBasic), char, slot_size - sizeof(struct RBasic)); + } // The husk keeps its original (larger) slot, so give it a field-less shape // sized to that slot; otherwise compaction's slot_size == shape_slot_size diff --git a/string.c b/string.c index ce001d35aaec5d..9488baeda468c4 100644 --- a/string.c +++ b/string.c @@ -229,6 +229,14 @@ rb_str_reembeddable_p(VALUE str) return !FL_TEST(str, STR_NOFREE|STR_SHARED_ROOT|STR_SHARED); } +/* True when other strings read this string's bytes out of its own slot, so the slot + * contents must stay valid for as long as the object does. */ +bool +rb_str_embedded_shared_root_p(VALUE str) +{ + return STR_EMBED_P(str) && FL_TEST(str, STR_SHARED_ROOT); +} + static inline size_t rb_str_embed_size(long capa, long termlen) { diff --git a/test/erb/test_erb.rb b/test/erb/test_erb.rb index 1de892544e5b10..6da25146ff7966 100644 --- a/test/erb/test_erb.rb +++ b/test/erb/test_erb.rb @@ -740,3 +740,43 @@ def teardown ERB::Compiler::Scanner.instance_variable_set('@scanner_map', @save_map) end end + +class TestERBRactor < Test::Unit::TestCase + def test_compile_and_result_in_ractor + assert_ractor(<<~RUBY, require: 'erb') + r = Ractor.new do + ERB.new("Hello, <%= 'world' %>!").result(binding) + end + assert_equal("Hello, world!", r.value) + RUBY + end + + def test_trim_mode_in_ractor + assert_ractor(<<~RUBY, require: 'erb') + src = "<% [1, 2].each do |i| %>\\n<%= i %>\\n<% end %>\\n" + r = Ractor.new(src) { |s| ERB.new(s, trim_mode: '-').result(binding) } + assert_equal("\\n1\\n\\n2\\n\\n", r.value) + + r = Ractor.new(src) { |s| ERB.new(s, trim_mode: '<>').result(binding) } + assert_equal("12", r.value) + RUBY + end + + def test_frozen_erb_instance_reused_across_ractors + assert_ractor(<<~RUBY, require: 'erb') + erb = ERB.new("<%= 1 + 1 %>") + erb.freeze + rs = 2.times.map { Ractor.new(erb) { |e| e.result(binding) } } + assert_equal(["2", "2"], rs.map(&:value)) + RUBY + end + + def test_util_html_escape_in_ractor + assert_ractor(<<~RUBY, require: 'erb') + r = Ractor.new do + ERB::Util.html_escape("