Skip to content
Merged
5 changes: 4 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ Note: We're only listing outstanding class updates.
* `Module#descendants` is added. It returns an array of classes and
modules that have the receiver in their ancestors. [[Feature #9779]]
* `Module#ruby2_keywords` and top-level `ruby2_keywords` are
deprecated and will be removed in Ruby 4.4. [[Feature #22205]]
deprecated and will be removed in Ruby 4.4. [[Feature #22205]]
* `Module#method_defined?` now accepts a third optional argument to also
match private methods. [[Feature #22297]]

* ObjectSpace

Expand Down Expand Up @@ -445,6 +447,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
[Feature #22205]: https://bugs.ruby-lang.org/issues/22205
[Feature #22226]: https://bugs.ruby-lang.org/issues/22226
[Feature #22238]: https://bugs.ruby-lang.org/issues/22238
[Feature #22297]: https://bugs.ruby-lang.org/issues/22297
[PR #17201]: https://github.com/ruby/ruby/pull/17201
[GH-psych #805]: https://github.com/ruby/psych/pull/805
[RubyGems-v4.0.4]: https://github.com/rubygems/rubygems/releases/tag/v4.0.4
Expand Down
10 changes: 10 additions & 0 deletions benchmark/struct_accessor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,21 @@ prelude: |
end
END
end
# 200 members do not fit in a GC slot, so this struct is not embedded
H = Struct.new(*(1..200).map { |i| :"m#{i}" }) do
class_eval <<-END
def w
#{'self.m200 = nil;'*256}
end
END
end
C.new(nil) # ensure common shape is known
obj = C.new(nil)
heap_obj = H.new
benchmark:
member_reader: "obj.r"
member_writer: "obj.w"
member_writer_heap: "heap_obj.w"
member_reader_method: "obj.rm"
member_writer_method: "obj.wm"
ivar_reader: "obj.r_ivar"
122 changes: 122 additions & 0 deletions bootstraptest/test_yjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3783,6 +3783,128 @@ def foo(s)
foo(s) rescue :ok
}

# struct aset returns the assigned value
assert_equal '42', %q{
def foo(s)
x = (s.foo = 42)
x
end

S = Struct.new(:foo)
foo(S.new)
foo(S.new)
}

# struct aset on a frozen struct raises FrozenError (embedded)
assert_equal 'ok', %q{
def foo(s)
s.foo = 123
end

S = Struct.new(:foo)
foo(S.new(1)) # compile the inline store on a non-frozen receiver
foo(S.new(2))
frozen = S.new(3).freeze
begin
foo(frozen)
:bad
rescue FrozenError
:ok
end
}

# struct aset on a frozen struct raises FrozenError (non-embedded)
assert_equal 'ok', %q{
def foo(s)
s.m1 = 1
end

max_alloc_size, rbasic_size, rvalue_overhead = GC::INTERNAL_CONSTANTS.fetch_values(
:RVARGC_MAX_ALLOCATE_SIZE,
:RBASIC_SIZE,
:RVALUE_OVERHEAD
) { skip(:ok) }
max_embedded_members = (max_alloc_size - rbasic_size - rvalue_overhead) / 8
S = Struct.new(*(1..(max_embedded_members + 1)).map { |i| :"m#{i}" })
foo(S.new) # compile the inline store on a non-frozen receiver
foo(S.new)
frozen = S.new.freeze
begin
foo(frozen)
:bad
rescue FrozenError
:ok
end
}

# struct aset writing nil and false skips the write barrier
assert_equal '[nil, false, true]', %q{
def foo(s)
s.a = nil
s.b = false
s.c = true
end

S = Struct.new(:a, :b, :c)
s = S.new(1, 2, 3)
foo(s)
s = S.new(1, 2, 3)
foo(s)
[s.a, s.b, s.c]
}

# struct aset writing heap objects exercises the write barrier and survives GC (embedded)
assert_equal '["foo", [1, 2], {k: :v}, "bar", "baz"]', %q{
def foo(s, a, b, c, d, e)
s.m1 = a
s.m2 = b
s.m3 = c
s.m4 = d
s.m5 = e
end

S = Struct.new(*(1..5).map { |i| :"m#{i}" })
s = S.new
foo(s, "f", [], {}, "b", "z") # compile
GC.start
GC.start # promote s to the old generation
foo(s, "fo" + "o", [1, 2], { k: :v }, "ba" + "r", "ba" + "z")
GC.start # a missing write barrier would lose the young objects here
[s.m1, s.m2, s.m3, s.m4, s.m5]
}

# struct aset via .send (VM_CALL_OPT_SEND)
assert_equal '7', %q{
def foo(s)
s.send(:foo=, 7)
end

S = Struct.new(:foo)
s = S.new
foo(s)
s = S.new
foo(s)
s.foo
}

# struct aset via .send on a frozen struct still raises FrozenError
assert_equal 'ok', %q{
def foo(s)
s.send(:foo=, 7)
end

S = Struct.new(:foo)
foo(S.new) # compile the .send path on a non-frozen receiver
foo(S.new)
frozen = S.new(1).freeze
begin
foo(frozen)
:bad
rescue FrozenError
:ok
end
}

# File.join is a cfunc accepting variable arguments as a Ruby array (argc = -2)
assert_equal 'foo/bar', %q{
def foo
Expand Down
59 changes: 53 additions & 6 deletions gc/default/default.c
Original file line number Diff line number Diff line change
Expand Up @@ -4094,6 +4094,47 @@ gc_abort(void *objspace_ptr)
gc_mode_set(objspace, gc_mode_none);
}

#if VERIFY_FREE_SIZE
# ifdef RB_THREAD_LOCAL_SPECIFIER
# define GC_FREEING_OBJ_TLS RB_THREAD_LOCAL_SPECIFIER
# else
# define GC_FREEING_OBJ_TLS
# endif

static GC_FREEING_OBJ_TLS VALUE gc_freeing_obj;

/* Remember what we are tearing down so that a bad xfree() underneath can name
* the object and not just the buffer. Saved and restored because a dfree
* callback can free another object. */
static bool
gc_obj_free(void *objspace, VALUE obj)
{
VALUE prev = gc_freeing_obj;
gc_freeing_obj = obj;

bool freed = rb_gc_obj_free(objspace, obj);

gc_freeing_obj = prev;
return freed;
}

static const char *
gc_freeing_obj_info(void)
{
/* Not thread-local: only reachable from a rb_bug() path, where a second
* thread racing us is already unrecoverable. */
static char buf[128];

if (!gc_freeing_obj) return NULL;

snprintf(buf, sizeof(buf), "%p %s", (void *)gc_freeing_obj, rb_obj_info(gc_freeing_obj));
return buf;
}
#else
# define gc_obj_free(objspace, obj) rb_gc_obj_free((objspace), (obj))
# define gc_freeing_obj_info() NULL
#endif

void
rb_gc_impl_shutdown_free_objects(void *objspace_ptr)
{
Expand All @@ -4110,7 +4151,7 @@ rb_gc_impl_shutdown_free_objects(void *objspace_ptr)
asan_unpoisoning_object(vp) {
if (RB_BUILTIN_TYPE(vp) != T_NONE) {
rb_gc_obj_free_vm_weak_references(vp);
if (rb_gc_obj_free(objspace, vp)) {
if (gc_obj_free(objspace, vp)) {
RBASIC(vp)->flags = 0;
}
}
Expand Down Expand Up @@ -4184,7 +4225,7 @@ rb_gc_impl_shutdown_call_finalizer(void *objspace_ptr)
asan_unpoisoning_object(vp) {
if (rb_gc_shutdown_call_finalizer_p(vp)) {
rb_gc_obj_free_vm_weak_references(vp);
if (rb_gc_obj_free(objspace, vp)) {
if (gc_obj_free(objspace, vp)) {
RBASIC(vp)->flags = 0;
}
}
Expand Down Expand Up @@ -4723,7 +4764,7 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit
gc_report(2, objspace, "page_sweep: free %p\n", (void *)p);

rb_gc_obj_free_vm_weak_references(vp);
if (rb_gc_obj_free(objspace, vp)) {
if (gc_obj_free(objspace, vp)) {
(void)VALGRIND_MAKE_MEM_UNDEFINED((void*)p, slot_size);
gc_sweep_register_free_slot(objspace, sweep_page, ctx, p, slot_size);
gc_report(3, objspace, "page_sweep: %s is freed\n", rb_obj_info(vp));
Expand Down Expand Up @@ -11125,11 +11166,15 @@ rb_gc_impl_free(void *objspace_ptr, void *ptr, size_t old_size)
struct malloc_obj_info *info = (struct malloc_obj_info *)ptr - 1;
#if VERIFY_FREE_SIZE
if (!info->size) {
rb_bug("buffer %p has no recorded size. Was it allocated with ruby_mimalloc? If so it should be freed with ruby_mimfree", ptr);
const char *freeing = gc_freeing_obj_info();
rb_bug("buffer %p has no recorded size%s%s. Was it allocated with ruby_mimalloc? If so it should be freed with ruby_mimfree", ptr,
freeing ? ", while freeing " : "", freeing ? freeing : "");
}

if (old_size && (old_size + sizeof(struct malloc_obj_info)) != info->size) {
rb_bug("buffer %p freed with old_size=%zu, but was allocated with size=%zu", ptr, old_size, info->size - sizeof(struct malloc_obj_info));
const char *freeing = gc_freeing_obj_info();
rb_bug("buffer %p freed with old_size=%zu, but was allocated with size=%zu%s%s", ptr, old_size, info->size - sizeof(struct malloc_obj_info),
freeing ? ", while freeing " : "", freeing ? freeing : "");
}
#endif
ptr = info;
Expand Down Expand Up @@ -11240,7 +11285,9 @@ rb_gc_impl_realloc(void *objspace_ptr, void *ptr, size_t new_size, size_t old_si
ptr = info;
#if VERIFY_FREE_SIZE
if (old_size && (old_size + sizeof(struct malloc_obj_info)) != info->size) {
rb_bug("buffer %p realloced with old_size=%zu, but was allocated with size=%zu", ptr, old_size, info->size - sizeof(struct malloc_obj_info));
const char *freeing = gc_freeing_obj_info();
rb_bug("buffer %p realloced with old_size=%zu, but was allocated with size=%zu%s%s", ptr, old_size, info->size - sizeof(struct malloc_obj_info),
freeing ? ", while freeing " : "", freeing ? freeing : "");
}
#endif
old_size = info->size;
Expand Down
3 changes: 2 additions & 1 deletion lib/erb/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
module ERB::Escape
# :stopdoc:
def html_escape(s)
CGI.escapeHTML(s.to_s)
s = s.to_s unless String === s
CGI.escapeHTML(s)
end
module_function :html_escape
end
Expand Down
50 changes: 50 additions & 0 deletions spec/ruby/core/module/method_defined_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,54 @@
ModuleSpecs::Child.method_defined?(:private_super_module, false).should == false
end
end

ruby_version_is "4.1" do
describe "when passed true as a third optional argument" do
it "returns true for private methods as well" do
# Include super
ModuleSpecs::Child.method_defined?(:public_child, true, true).should == true
ModuleSpecs::Child.method_defined?(:protected_child, true, true).should == true
ModuleSpecs::Child.method_defined?(:accessor_method, true, true).should == true
ModuleSpecs::Child.method_defined?(:private_child, true, true).should == true
ModuleSpecs::Child.method_defined?(:undefined, true, true).should == false

# Defined in Parent
ModuleSpecs::Child.method_defined?(:public_parent, true, true).should == true
ModuleSpecs::Child.method_defined?(:protected_parent, true, true).should == true
ModuleSpecs::Child.method_defined?(:private_parent, true, true).should == true

# Defined in Module
ModuleSpecs::Child.method_defined?(:public_module, true, true).should == true
ModuleSpecs::Child.method_defined?(:protected_module, true, true).should == true
ModuleSpecs::Child.method_defined?(:private_module, true, true).should == true

# Defined in SuperModule
ModuleSpecs::Child.method_defined?(:public_super_module, true, true).should == true
ModuleSpecs::Child.method_defined?(:protected_super_module, true, true).should == true
ModuleSpecs::Child.method_defined?(:private_super_module, true, true).should == true

# Ignore super
ModuleSpecs::Child.method_defined?(:public_child, false, true).should == true
ModuleSpecs::Child.method_defined?(:protected_child, false, true).should == true
ModuleSpecs::Child.method_defined?(:accessor_method, false, true).should == true
ModuleSpecs::Child.method_defined?(:private_child, false, true).should == true
ModuleSpecs::Child.method_defined?(:undefined, false, true).should == false

# Defined in Parent
ModuleSpecs::Child.method_defined?(:public_parent, false, true).should == false
ModuleSpecs::Child.method_defined?(:protected_parent, false, true).should == false
ModuleSpecs::Child.method_defined?(:private_parent, false, true).should == false

# Defined in Module
ModuleSpecs::Child.method_defined?(:public_module, false, true).should == false
ModuleSpecs::Child.method_defined?(:protected_module, false, true).should == false
ModuleSpecs::Child.method_defined?(:private_module, false, true).should == false

# Defined in SuperModule
ModuleSpecs::Child.method_defined?(:public_super_module, false, true).should == false
ModuleSpecs::Child.method_defined?(:protected_super_module, false, true).should == false
ModuleSpecs::Child.method_defined?(:private_super_module, false, true).should == false
end
end
end
end
9 changes: 9 additions & 0 deletions test/erb/test_erb_escape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ def test_html_escape
assert_equal(65536+5, ERB::Util.html_escape("&" + "x"*65536).size)
end

def test_html_escape_string_subclass
klass = Class.new(String) do
def to_s
"<to_s>"
end
end
assert_equal("&lt;b&gt;", ERB::Util.html_escape(klass.new("<b>")))
end

def test_html_escape_to_s
object = Object.new
def object.to_s
Expand Down
20 changes: 17 additions & 3 deletions test/json/json_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -539,18 +539,32 @@ def test_configure_keeps_the_layout_of_a_pretty_state
end

def test_configure_only_writes_the_other_options_it_is_given
omit 'JRuby resets the non-string options' if RUBY_ENGINE == 'jruby'
state = JSON.state.new(max_nesting: 3, allow_nan: true, ascii_only: true, script_safe: true)
state = JSON.state.new(max_nesting: 3, allow_nan: true, ascii_only: true, script_safe: true,
strict: true, buffer_initial_length: 32)
state.configure(indent: '1')
assert_equal '1', state.indent
assert_equal 3, state.max_nesting
assert_equal true, state.allow_nan?
assert_equal true, state.ascii_only?
assert_equal true, state.script_safe?
assert_equal true, state.strict?
assert_equal 32, state.buffer_initial_length
end

def test_configure_keeps_sort_keys
state = JSON.state.new(sort_keys: true)
state.configure(depth: 0)
assert_equal '{"a":2,"b":1}', state.generate({ 'b' => 1, 'a' => 2 })
end

def test_configure_keeps_as_json
as_json = ->(object, _is_key) { object.to_s }
state = JSON.state.new(strict: true, as_json: as_json)
state.configure(depth: 0)
assert_equal as_json, state.as_json
end

def test_configure_writes_a_string_option_given_as_nil
omit 'JRuby keeps the previous value for an explicit nil' if RUBY_ENGINE == 'jruby'
state = JSON.state.new(indent: '1', space: '2')
state.configure(indent: nil)
assert_equal '', state.indent
Expand Down
Loading