Repository navigation
wave 32 (lang): singleton classes of nil/true/false, post parameters, !~/!/!=, and a refinement that reaches its own blocks - #665
Merged
Merged
Conversation
…es Class
MRI read: class.c v3_4_0 — special_singleton_class_of (:2199),
rb_special_singleton_class (:2210), singleton_class_of (:2224-2260),
rb_singleton_class (:2299) and make_metaclass (:766-792).
Two defects, one file apart, both measured through the repaired ruby/spec shim
against MRI 4.0.5 run through the same shim.
1. `nil.singleton_class` raised TypeError.
singleton_class_of has TWO arms for immediates. The first raises "can't
define singleton" — T_FIXNUM, T_BIGNUM, T_FLOAT, T_SYMBOL (class.c:2228).
The second, just below it, answers special_singleton_class_of for T_FALSE /
T_TRUE / T_NIL (class.c:2236): nil, true and false have no room for a
per-object singleton, so Ruby hands back their CLASS — NilClass, TrueClass,
FalseClass. rbgo drew no such line and raised for all seven.
specialSingletonClass is that function; ensureSingleton consults it first, so
every door into a singleton class opens the same one: #singleton_class,
`class << nil`, #define_singleton_method, #extend and #instance_eval. A
method defined "on" nil is therefore an ordinary instance method of NilClass,
reached by every nil in the program, which is what MRI does.
Note the two classes are NOT singleton classes: #singleton_class? is false
and #superclass is Object. A fabricated singleton would have answered both
the other way.
2. `BasicObject.singleton_class.superclass` was nil.
make_metaclass ends the metaclass chain at Class:
RCLASS_SET_SUPER(metaclass, super ? ENSURE_EIGENCLASS(super) : rb_cClass)
(class.c:786)
BasicObject is the only class with no superclass, so one missing link left
every singleton class of a class cut off from Class: #ancestors did not
include it, its instance methods were not #method_defined?, and its class
methods drew no #respond_to?. closeMetaclassChain wires it once at boot,
before any metaclass is asked for, since RClass.metaClass cannot reach cClass
from where it runs.
Measured, per file, rbgo before -> after against MRI 4.0.5 through the same
shim:
language/singleton_class_spec.rb 35 -> 47 (MRI 53)
language/metaclass_spec.rb 18 -> 21 (MRI 24)
language/variables_spec.rb 113 -> 114 (MRI 118)
language/ total 2314 -> 2330 (MRI 2627)
No file regressed.
TestSingletonClassErrors pinned `class << nil` as a TypeError — it pinned the
divergence. Updated to the MRI-verified set (Fixnum, Symbol, Float, Bignum: the
raising arm, all four now covered) with the nil/true/false side moved to
TestSpecialSingletonClass.
Left, all of them behind builtins.go, which another agent owns this wave:
#is_a? resolves through classOf rather than the singleton class, so
`o.is_a?(o.singleton_class)` and the two "subclass of Class's singleton class"
examples still fail; Class#new / #allocate do not refuse a singleton class (and
#new panics on one, recovered as an ArgumentError); and a frozen object's
singleton class is not frozen. Six examples in singleton_class_spec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hem after a splat MRI read: rb_iseq_min_max_arity (proc.c v3_4_0:1073-1080), rb_iseq_parameters (iseq.c v3_4_0:3578-3660) and setup_parameters_complex / args_setup_post_parameters (vm_args.c v3_4_0:595, 878-892). param.post_num is the count of positional parameters that bind from the TAIL, and it is independent of has_rest: `def m(a, *r, c)` has post_num 1 and so does `def m(a=1, b)`. Neither rb_iseq_min_max_arity's `lead_num + post_num` nor rb_iseq_parameters' `[post_start, post_start+post_num) -> :req` consults the splat. rbgo had each half wrong in a DIFFERENT place, in opposite directions, which is why #650 — which fixed Proc#arity for the no-splat shape — could not have found either: * the METHOD half (iseqRequiredPositional, iseqHasOptional, buildParamsList) knew only the after-splat post run. `def m(a=1, b)` answered arity -1 for MRI's -2 and parameters [[:opt, :a], [:opt, :b]] for MRI's [[:opt, :a], [:req, :b]] — the post parameter reported as optional. * the PROC half (Proc#arityVal) read is.PostCount directly, which is 0 whenever there IS a splat. `proc { |a, *b, c| }.arity` answered -2 for MRI's -3, and so did the lambda. iseqPostCount already reported both shapes; every reader now goes through it, and iseqPostStart names the index rb_iseq_parameters walks from. One rule, one place: the binder in exec was already right, and reads the same pair. That last part cost a defect on the way in. iseqRequiredPositional is now lead+post, so exec's `minReq := iseqRequiredPositional(iseq) + iseq.PostCount` counted the no-splat post run twice and rejected `m(9)` for `def m(a=1, b)` with "given 1, expected 2". TestPostParamsBindFromTheTail is the control that caught it — the reflective counts are worth nothing unless the binder agrees with them, so it calls the methods rather than only asking their arity. Verified byte-for-byte against MRI 4.0.5 across the arity/parameters matrix (def, define_method, lambda, non-lambda proc, Method#to_proc, UnboundMethod, splat and no-splat, keywords and block). language/ total unchanged at 2330: core/method/arity_spec.rb and core/proc/arity_spec.rb both stop on a shared- example the shim does not implement — under MRI too — so the win here is behavioural and reflective rather than one the ratchet can show. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MRI read: object.c v3_4_0 — rb_obj_not (:264), rb_obj_not_equal (:280),
rb_obj_not_match (:1678), and the three rb_define_method calls that install
them on BasicObject and Kernel (:4265, :4266, :4427); compile.c v3_4_0's
specialisation table (:4260-4297); vm_insnhelper.c v3_4_0 vm_opt_not (:7019)
and vm_opt_neq (:6665).
In MRI none of the three is a compiler-synthesised negation. All three are real
methods. `!` and `!=` get SPECIALISED instructions, opt_not and opt_neq, which
compute the answer in place only while the receiver's method is still the
built-in cfunc (vm_method_cfunc_is) and otherwise fall back to the send. `!~`
gets no specialisation at all: idNMatch is absent from the opt_* table, so
every `a !~ b` is an ordinary method call.
The issue asked whether one cause had produced three defects. Measured against
MRI 4.0.5 it had produced three, but each with a different shape — so fixing
only the one the issue named would have left two:
* `!~` was compiled to `!(a =~ b)`. A user-defined #!~ was DEAD CODE, and so
was Kernel#!~ itself: `Object.new.send(:!~, 1)` reported '!~' missing where
MRI reports '=~'. This is what inverted 11 assertions in the conformance
shim, which spells `!~` on its ShouldProxy.
* unary `!` was inlined UNCONDITIONALLY. A redefined #! never ran, anywhere —
not from a class, not from a singleton, not from a module.
* `!=` dispatched, but hasCustomNeq walked vm.classOf(a).super only. A
singleton `def o.!=` and a module-supplied one (o.extend M) were both
invisible and the opcode inverted #== instead. Half right is the hardest
kind to notice: the class case worked, so the mechanism looked present.
One resolver now answers all three. overriddenNegation resolves the name
through the FULL dispatch chain — singleton class and extended modules first,
then the class ancestry — and reports it only when it is not the BasicObject
default, which is vm_method_cfunc_is. notValue is opt_not; hasCustomNeq is one
line over the same resolver; the compiler emits a plain send for `!~`.
Kernel#!~ goes in the prelude, in Ruby, because the body IS Ruby: send #=~ and
negate, so a redefined #=~ or a mock expecting one is what runs. That puts the
whole reflective surface where MRI has it — #respond_to?(:!~),
Kernel.instance_method(:!~), #method(:!~), and #send.
Three call sites had to move together or the interpreter and the AOT-lowered
bodies would answer differently for the same source: internal/aot/codegen.go
and level2.go now emit vm.notValue rather than an inline negation. Their two
pinned expectations were updated to the MRI-verified form. The frozen prelude
was regenerated and is NOT byte-identical: it carries the new <module:Kernel> /
!~ ISeqs, which is the check that the emitter saw the addition.
Cost: an opt_not now consults a per-instruction inline cache (the CALL_DATA
MRI's opt_not carries) instead of negating blind. Measured serially, three runs
each, on a loop that does nothing but negate 10M times: 1.11s -> 1.20s user,
about +8% of a synthetic worst case.
Measured, per file, rbgo before -> after against MRI 4.0.5 through the same
shim:
core/kernel/not_match_spec.rb 2 (+1 error) -> 3 (MRI 3)
core/exception/full_message_spec.rb 4 -> 8 (MRI 20)
language/match_spec.rb 1 -> 2 (MRI 7)
language/defined_spec.rb 255 -> 256 (MRI 257)
language/ total 2330 -> 2332 (MRI 2627)
core/basicobject/not_spec.rb and not_equal_spec.rb were already at MRI's 2 and
8 and stayed there: they only exercise the defaults.
Left: Object.new.method(:!~).parameters answers [[:req, :other]] where MRI
answers [[:req]], because MRI's is a cfunc with no parameter name and a Ruby
definition must spell one. Nothing in language/ or core/ reads it.
Touches two files outside the wave's exclusive list — internal/vm/arith.go (one
function body, where hasCustomNeq lives) and internal/vm/prelude.rb (one
method appended). Neither is on the do-not-touch list; fixing `!=` and giving
`!~` a real method could not be done from the exclusive set alone, and leaving
either half would have left the other lying.
Closes #663.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… in it
MRI read: rb_vm_cref_replace_with_duplicated_cref (vm.c v3_4_0:1931) →
vm_cref_replace_with_duplicated_cref (vm_insnhelper.c v3_4_0:912), called from
mod_using (eval.c v3_4_0:1547) and top_using (eval.c v3_4_0:1890); vm_cref_push
and its pushed_by_eval flag (vm_insnhelper.c); rb_yield_refine_block and
rb_mod_refine (eval.c).
`using` does not merely record a module. It REPLACES the calling frame's cref
with a duplicate that carries the refinements, so every block created after it
in that frame captures the replacement and every scope pushed inside it chains
to it. MRI then walks the whole chain in
rb_method_entry_with_refinements.
rbgo records one *RClass per frame, and adoptRefinementCref already performed
that replacement on the frame's cref ENTRY — which is why `1.foo` written
DIRECTLY in a `Module.new { using R; … }` body already worked. What did not
work was everything written inside it: the block-literal path read the lexCref
LOCAL, settled before `using` ran and frozen at where the block was written
(top level, for a Module.new body). Measured against MRI 4.0.5, a refinement
activated in such a body reached NO nested block at all — not Module.new {},
not Class.new {}, not #class_eval or #module_eval, not #instance_eval or
#instance_exec, not even `[0].each { }`.
Three links, because rbgo has no chain and MRI's has to be approximated by its
ends:
1. frameCref() reads the frame's cref ENTRY rather than the local, so a block
literal captures the post-`using` replacement, as MRI captures it at
block-creation time;
2. refinedFor consults the frame's DEFINEE and then that cref. The definee is
MRI's pushed link — the refinement module inside a `refine` block, the
eval target inside a class_eval — and the cref is the textual one it was
pushed onto. Consulting only the cref cost 3 examples in refine_spec,
because a refine block's own sibling refinements hang off the pushed link;
consulting only the definee was the original defect. Both, innermost
first, is what MRI's walk does.
3. Proc.refDefinee carries the second end onto a block, because a method of a
class whose body called `using` has the TEXTUAL scope as its frame cref —
so a lambda written in that method had no link back at all.
refDefinee feeds refinement lookup ONLY. Constant resolution still follows cref
alone, because MRI SKIPS a pushed_by_eval cref when resolving a constant, and
TestCrefChainKeepsItsOtherEnd is the control for that: a constant inside a
class_eval block still resolves textually, Module.nesting is still the textual
nesting, and a `def` there still lands on the eval target.
Measured, per file, rbgo before -> after against MRI 4.0.5 through the same
shim:
core/module/using_spec.rb 14 -> 17 (MRI 20)
core/module/refine_spec.rb 29 -> 30 (MRI 38)
core/ total 19922 -> 19926
No file regressed; language/ is unchanged at 2332 (the shapes are all in core/).
Left in using_spec: "is active for block called via instance_eval", where the
receiver is a String the method holds in an ivar — a fourth link this
two-ended approximation does not reach; and the two "is not active …"
examples, which were failing before this change and are over-activation, not
under-. Most of refine_spec's remaining gap is indirect dispatch — #send,
#respond_to?, #method, Symbol#to_proc, #binding honouring refinements — which
lives in refinement.go, owned by another agent this wave.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Sep 26, 2026
Three units, +283 measured, and the first wave whose numbers are trustworthy throughout: #660 repaired the harness at the end of wave 31, so this is the first measurement in the campaign with no known vacuous assertions under it. #666 core/kernel, numerics, symbols (+162). 45 of those were INVISIBLE rather than failing: core/integer/{pow,exponent}_spec.rb each scored 0 because the corpus contains `100000000 ** 1000000000` and rbgo computed it unbounded until the harness killed the process. bignum.c's BIGLEN_LIMIT refuses it. core/rational and core/builtin_constants are now AT the ceiling. #668 IO/Dir encodings resolved once at open, #syswrite reaching the descriptor, IO::Buffer's base/size distinction (+56). THIRTEEN of the sixteen files it touched now sit exactly at the ceiling the harness can express. #665 singleton classes of nil/true/false, post parameters in Method#arity AND #parameters together, and `!~`/`!`/`!=` DISPATCHED rather than synthesised — one cause, three defects, found by asking whether #663's neighbours shared its shape. Measured with scripts/conformance/rubyspec/run.sh against the pinned corpus at 87b1631, 16-way, TIMEOUT=25, three consecutive runs: 22489, 22489, 22488. The floor is set thirteen below the lowest. Three of my briefing targets dissolved under measurement again — the third wave running. core/io/write_spec's "9 failures" is a real gap of 1 (MRI fails the other eight through this shim); core/array/fill_spec's "9" is a gap of ZERO (ceiling 38, rbgo 38); core/numeric/step_spec's 12.700000000000001 is what MRI prints. Running MRI through our own shim for a per-file ceiling is what catches these, and it is now done before planning rather than after. The largest lever left is not in any of these lanes: core/file/printf_spec.rb scores 0 against a ceiling of 160 because rbgo cannot parse a do...end block inside an arrow-lambda that is a paren-less command argument. One parser fix, 160 examples — issue #669. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
wave 32 (lang): singleton classes of nil/true/false, post parameters,
!~/!/!=, and a refinement that reaches its own blocksFour measured defects, each reproduced against MRI 4.0.5 through the same shim before it was touched, and each cited to the MRI source it was read from.
Totals, rbgo before → after, against MRI 4.0.5 run through the same shim:
language/core/No file regressed in either tree, and
core/'s 14 load failures are the same 14 (all pre-existing parse errors or signal/exit specs).FLOORis untouched, as asked; it sits at 22205 and the measured total is 53 above it.1. Singleton classes of
nil/true/false, and a metaclass chain that reachesClassMRI read:
class.cv3_4_0 —special_singleton_class_of(:2199),singleton_class_of(:2224-2260),rb_singleton_class(:2299),make_metaclass(:766-792).singleton_class_ofhas two arms for immediates. The first raises"can't define singleton"forT_FIXNUM,T_BIGNUM,T_FLOAT,T_SYMBOL. The second, just below, answersspecial_singleton_class_offorT_FALSE/T_TRUE/T_NIL: those three have no room for a per-object singleton, so Ruby hands back their class. rbgo drew no line and raised for all seven.Every door now opens the same class —
#singleton_class,class << nil,#define_singleton_method,#extend,#instance_eval— so a method defined "on" nil is an ordinary instance method ofNilClass, as in MRI.Separately,
make_metaclassends the metaclass chain atClass:RCLASS_SET_SUPER(metaclass, super ? ENSURE_EIGENCLASS(super) : rb_cClass). BasicObject is the only class with no superclass, so one missing link left every singleton class of a class cut off fromClass— not in#ancestors, no#method_defined?, no#respond_to?.language/singleton_class_spec.rblanguage/metaclass_spec.rbcore/kernel/singleton_class_spec.rbcore/module/ancestors_spec.rbcore/{nil,true,false}/singleton_method_spec.rbcore/basicobject/basicobject_spec.rblanguage/variables_spec.rbTestSingletonClassErrorspinnedclass << nilas a TypeError — it pinned the divergence. It was updated to the MRI-verified raising set (Fixnum, Symbol, Float, Bignum, all four now covered).2.
Method#arity/#parameterssee post parameters, andProc#aritysees them after a splatMRI read:
rb_iseq_min_max_arity(proc.cv3_4_0:1073-1080),rb_iseq_parameters(iseq.cv3_4_0:3578-3660),setup_parameters_complex/args_setup_post_parameters(vm_args.cv3_4_0:595, 878-892).param.post_numis independent ofhas_rest:def m(a, *r, c)has post_num 1 and so doesdef m(a=1, b). rbgo had each half wrong in a different place, in opposite directions — which is why #650, which fixedProc#arityfor the no-splat shape, could not have found either:def m(a=1, b)answered arity-1for MRI's-2, and[[:opt, :a], [:opt, :b]]for MRI's[[:opt, :a], [:req, :b]];is.PostCountdirectly, which is 0 whenever there is a splat:proc { |a, *b, c| }.arityanswered-2for MRI's-3.Both now read
iseqPostCount, which already reported both shapes. Verified byte-for-byte against MRI across the whole matrix (def,define_method, lambda, non-lambda proc,Method#to_proc,UnboundMethod, splat and no-splat, keywords, block).This cost a defect on the way in, and
TestPostParamsBindFromTheTailis the control that caught it: makingiseqRequiredPositionallead+post leftexec'sminReq := iseqRequiredPositional(iseq) + iseq.PostCountcounting the post run twice, which rejectedm(9)fordef m(a=1, b). Reflective counts are worth nothing unless the binder agrees with them, so the control calls the methods.core/method/arity_spec.rbandcore/proc/arity_spec.rbstop on a shared-example the shim does not implement — under MRI too — so this one is behavioural and reflective rather than something the ratchet can show.3. #663 —
!~,!and!=are dispatched, not synthesisedMRI read:
object.cv3_4_0rb_obj_not(:264),rb_obj_not_equal(:280),rb_obj_not_match(:1678) and the threerb_define_methodcalls (:4265, :4266, :4427);compile.cv3_4_0's specialisation table (:4260-4297);vm_insnhelper.cv3_4_0vm_opt_not(:7019) andvm_opt_neq(:6665).None of the three is a compiler-synthesised negation in MRI.
!and!=get specialisations (opt_not,opt_neq) that compute in place only while the receiver's method is still the built-in cfunc and otherwise fall back to the send.!~gets no specialisation at all.The issue asked whether one cause had produced three defects. Measured, it had produced three — each with a different shape, so fixing only the one the issue named would have left two:
!~compiled to!(a =~ b): a user-defined#!~was dead code, and so wasKernel#!~itself. This is what inverted 11 assertions in the conformance shim.!was inlined unconditionally: a redefined#!never ran anywhere.!=dispatched, but only for a method on the class chain below BasicObject — a singletondef o.!=and a module-supplied one (o.extend M) were both invisible. Half right is the hardest kind to notice.One resolver answers all three now (
overriddenNegation, the equivalent ofvm_method_cfunc_is).Kernel#!~goes in the prelude, in Ruby, because the body is Ruby — so a redefined#=~, or a mock expecting one, is what runs, and#respond_to?,#method,#sendandKernel.instance_method(:!~)all answer as MRI's do.internal/aot/codegen.goandlevel2.gohad to move with the interpreter or a lowered body would answer differently for the same source; their two pinned expectations were updated. The frozen prelude was regenerated and is not byte-identical — it carries the new<module:Kernel>/!~ISeqs, which is the check that the emitter saw the addition.Cost, measured serially, three runs each, on a loop that does nothing but negate 10M times: 1.11s → 1.20s user, ≈ +8% of a synthetic worst case.
core/kernel/not_match_spec.rbcore/exception/full_message_spec.rblanguage/match_spec.rblanguage/defined_spec.rb4. A refinement activated in a block body now reaches the blocks written in it
MRI read:
rb_vm_cref_replace_with_duplicated_cref(vm.cv3_4_0:1931) →vm_cref_replace_with_duplicated_cref(vm_insnhelper.cv3_4_0:912), frommod_using(eval.c:1547) andtop_using(eval.c:1890);vm_cref_pushandpushed_by_eval;rb_yield_refine_block.usingreplaces the calling frame's cref with a duplicate carrying the refinements, so every block created after it captures the replacement and MRI walks the whole chain. rbgo already performed that replacement on the frame's cref entry — which is why1.foowritten directly in aModule.new { using R; … }body worked. Everything written inside it did not: the block-literal path read thelexCreflocal, settled beforeusingran. A refinement activated in such a body reached no nested block at all — notModule.new {}, notClass.new {}, not#class_eval, not#instance_eval/#instance_exec, not even[0].each { }.rbgo keeps one
*RClassper frame, so MRI's chain is approximated by its two ends — the frame's definee (MRI's pushed link: the refinement inside arefineblock, the eval target inside aclass_eval) and its cref (the textual one it was pushed onto), innermost first — plusProc.refDefinee, which carries the second end onto a block so a lambda written in a method of the class still has a link back.Consulting only the cref cost 3 examples in
refine_spec(a refine block's sibling refinements hang off the pushed link); consulting only the definee was the original defect.TestCrefChainKeepsItsOtherEndis the control for the other thing a cref is for: a constant inside aclass_evalblock still resolves textually, because MRI skips apushed_by_evalcref when resolving one.core/module/using_spec.rbcore/module/refine_spec.rbWhat was left, and why
language/method_spec.rbstayed at 55 of 62. Reproducing it first overturned the premise: 9 of its 16 non-passes are errors MRI has too (a shared-example the shim does not implement). Of the 7 real ones, 5 are the:strict_unused_blockwarning category and 2 are keyword/positional Hash separation. Neither is the arity defect, and the warning category lives in files another agent owns this wave.language/singleton_class_spec.rball needbuiltins.go:#is_a?resolves throughclassOfrather than the singleton class;Class#new/#allocatedo not refuse a singleton class (and#newpanics on one, recovered as an ArgumentError — worth its own issue); and a frozen object's singleton class is not frozen.Object.new.method(:!~).parametersanswers[[:req, :other]]where MRI answers[[:req]]: MRI's is a cfunc with no parameter name and a Ruby definition must spell one. Nothing inlanguage/orcore/reads it.Module(Module.new.singleton_class.superclass). The class root is fixed once at boot; the module case needscModulewhereRClass.metaClasscannot reach it. No example in either tree asks.def nil.foodoes not parse — the parser refusesnilas a def receiver. That is the external parser package, not this repo.core/module/using_spec.rb's remaining 3: oneinstance_evalshape needing a fourth chain link, and two over-activation examples that were already failing.refine_spec's gap is indirect dispatch (#send,#respond_to?,#method,Symbol#to_proc,#bindinghonouring refinements), which lives inrefinement.go.TestWave28KernelSleepSchedulerpinssleep(o)to"0"for a 0.001s sleep, while the neighbouringTestWave28SleepReturnsWholeSecondsdocuments that a sub-second sleep "can only answer 0 or 1". It failed once here and passed 5× on a rerun and 5× on unmodifiedmain.Files outside the wave's exclusive set
Two, both minimal and neither on the do-not-touch list:
internal/vm/arith.go(one function body —hasCustomNeqlives there) andinternal/vm/prelude.rb(one method appended). Fixing!=and giving!~a real method could not be done from the exclusive set alone, and leaving either half would have left the other lying.🤖 Generated with Claude Code