You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Set cannot be subclassed: Set.new and Set.[] ignore the receiver, so every subclass instance is a plain Set #903
Set cannot be subclassed. Every instance of a subclass is a plain Set.
require"set"classSub < Set;endSub[1,2].class# MRI 4.0.7: Sub rbgo: SetSub.new([1,2]).class# MRI 4.0.7: Sub rbgo: Set
Measured with rubydiff against /opt/homebrew/opt/ruby@4.0/bin/ruby (ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin25]) on main at 6e86fac3.
Cause
Both constructors are smethods on Set that ignore the receiver — the _ object.Value in each signature — and allocate with newSet():
A subclass inherits those singleton methods, so Sub.new and Sub[…] both reach newSet() and the subclass is lost at allocation. Nothing downstream can recover it.
What it hides
Everything that propagates the receiver's class through Set. The ones measured so far:
MRI propagates it deliberately: set_i_classify passes rb_obj_class(set) to its subset allocation (set.c:890), and set_divide_arity2 calls set_s_alloc(set_class) (set.c:953). Note the asymmetry those two lines encode — the subsets take the receiver's class while the outer Set is always rb_cSet (set.c:946, set.c:1002). A fix must keep that asymmetry; making everything the subclass would be just as wrong.
The prelude already writes self.class.new(...) in the right places, so those sites need no change once allocation honours the receiver.
Found while
Writing #902 (Set#divide). Its test cannot assert the subsets' class until this is fixed, and that is stated in the PR rather than worked around. #902 does not depend on this one.
Scope
This is allocation, not divide: newSet() would need to take the class the method was called on, and *Set would need to carry it. Also worth checking at the same time whether Sub.new's initialize and the other smethods on Set have the same shape.
⛔ Correcting the cause in this issue: it is not the two constructors
This issue says:
Both constructors are smethods on Set that ignore the receiver — the _ object.Value in each signature — and allocate with newSet()
That is true and it is not the cause. Fixing those two signatures would change nothing observable, because the class is not lost at allocation — it is unrepresentable:
// internal/vm/set.go:25typeSetstruct {
h*object.Hash// member -> true, keyed by the member's #hash/#eql?iteratingint// >0 while an #each is in progress (structural-mod guard)
}
There is no class field, so classOf cannot answer anything else:
A constructor that honoured its receiver would hand the class to a struct with nowhere to put it, and Sub[1,2].class would still be Set.
What the change actually is
add a class to Set and read it in classOf — that is the fix;
thread it through the 6newSet( call sites in internal/vm;
the two smethods then stop ignoring their receiver, which is the part this issue already describes.
What must not be lost
MRI's asymmetry still holds and is the thing to assert: the subsets take the receiver's class while the outer Set is always rb_cSet (set.c:946, set.c:1002). The prelude already writes self.class.new(...) for the subsets and a bare Set.new(...) for the outer one (#902), so once allocation carries the class this comes out right without any prelude change — which is exactly why it needs a test. Behaviour that is correct by accident of another change is behaviour nobody is holding in place.
Why this was filed in the wrong place
I read the two constructors, saw them discard the receiver, and stopped — a true observation that explains the symptom and is not its cause. The question that would have reached it in one step is "where does this value come from?" rather than "which function looks wrong?". Same shape as #907, where a fix in exceptionObject changed nothing because the exceptions in question never passed through it.
Nothing here is urgent and nothing is blocked; the diagnosis is corrected so the next reader does not start at the constructors and find them innocent.
Activity
tannevaled commentedon Nov 7, 2026
⛔ Correcting the cause in this issue: it is not the two constructors
This issue says:
That is true and it is not the cause. Fixing those two signatures would change nothing observable, because the class is not lost at allocation — it is unrepresentable:
There is no class field, so
classOfcannot answer anything else:A constructor that honoured its receiver would hand the class to a struct with nowhere to put it, and
Sub[1,2].classwould still beSet.What the change actually is
Setand read it inclassOf— that is the fix;newSet(call sites ininternal/vm;smethodsthen stop ignoring their receiver, which is the part this issue already describes.What must not be lost
MRI's asymmetry still holds and is the thing to assert: the subsets take the receiver's class while the outer Set is always
rb_cSet(set.c:946,set.c:1002). The prelude already writesself.class.new(...)for the subsets and a bareSet.new(...)for the outer one (#902), so once allocation carries the class this comes out right without any prelude change — which is exactly why it needs a test. Behaviour that is correct by accident of another change is behaviour nobody is holding in place.Why this was filed in the wrong place
I read the two constructors, saw them discard the receiver, and stopped — a true observation that explains the symptom and is not its cause. The question that would have reached it in one step is "where does this value come from?" rather than "which function looks wrong?". Same shape as #907, where a fix in
exceptionObjectchanged nothing because the exceptions in question never passed through it.Nothing here is urgent and nothing is blocked; the diagnosis is corrected so the next reader does not start at the constructors and find them innocent.
🤖 Generated with Claude Code