Fix escaped blocks in Enumerable#uniq

Keep the state for block-based `Enumerable#uniq` alive after it returns,
as `each` can retain and later invoke the internal block, making the
stack-allocated memo introduced in 29c669bb56 invalid.
This commit is contained in:
Nobuyoshi Nakada authored and Nobuyoshi Nakada committed 2026-09-24 19:22:12 +09:00
1 parent ea2836f7ec
commit 34d2105de0
2 files changed
+17 -16
Notes: git 2026-09-24 10:22:52 +00:00

No files matched your search

+8 -16
View File
@@ -4878,19 +4878,11 @@ uniq_func(RB_BLOCK_CALL_FUNC_ARGLIST(i, set))
return Qnil;
}
struct uniq_iter_memo {
VALUE set;
VALUE ary;
};
static VALUE
uniq_iter(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo_))
uniq_iter(RB_BLOCK_CALL_FUNC_ARGLIST(i, hash))
{
struct uniq_iter_memo *memo = (struct uniq_iter_memo *)memo_;
ENUM_WANT_SVALUE();
if (rb_set_add_no_check(memo->set, rb_yield_values2(argc, argv))) {
rb_ary_push(memo->ary, i);
}
rb_hash_add_new_element(hash, rb_yield_values2(argc, argv), i);
return Qnil;
}
@@ -4918,18 +4910,18 @@ uniq_iter(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo_))
static VALUE
enum_uniq(VALUE obj)
{
VALUE ret;
if (rb_block_given_p()) {
struct uniq_iter_memo memo;
memo.set = rb_obj_hide(rb_set_new());
memo.ary = rb_ary_new();
rb_block_call(obj, id_each, 0, 0, uniq_iter, (VALUE)&memo);
return memo.ary;
VALUE hash = rb_obj_hide(rb_hash_new());
rb_block_call(obj, id_each, 0, 0, uniq_iter, hash);
ret = rb_hash_values(hash);
}
else {
VALUE set = rb_obj_hide(rb_set_new());
rb_block_call(obj, id_each, 0, 0, uniq_func, set);
return rb_set_to_a(set);
ret = rb_set_to_a(set);
}
return ret;
}
static VALUE
+9
View File
@@ -1295,6 +1295,15 @@ class TestEnumerable < Test::Unit::TestCase
olympics.uniq{|k,v| v})
assert_equal([1, 2, 3, 4, 5, 10], (1..100).uniq{|x| (x**2) % 10 }.first(6))
assert_equal([1, [1, 2]], Foo.new.to_enum.uniq)
b = Struct.new(:block) do
def each(&blk)
self.block = blk
nil
end
end.new
b.uniq {|x| x}
assert_nil(b.block.call(:foo))
end
def test_compact