Skip to content

Enumerable#to_set with arguments: MRI 4.0 warns, rbgo is silent #909

Open

Description

@tannevaled

Enumerable#to_set with arguments is deprecated in 4.0 and MRI says so on stderr. rbgo says nothing.

require "set"
class Sub < Set; end
[1, 2].to_set(Sub)
$ /opt/homebrew/opt/ruby@4.0/bin/ruby probe.rb
probe.rb:3: warning: passing arguments to Enumerable#to_set is deprecated

$ rbgo probe.rb
(nothing)

Measured through scripts/conformance/rubyspec/cmd/oracle against
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin25]. Only stderr differs; stdout is byte-identical, and the resulting Set is correct in both (Sub).

It is Enumerable#to_set, not Set#to_set

Localised one call at a time rather than inferred, because the warning text names Enumerable while the obvious suspect is the method that takes the arguments:

call MRI warns?
[1, 2].to_set(Sub) — Enumerable, with a klass yes
[1, 2].to_set — Enumerable, bare no
Set[1, 2].to_set(Sub) — Set, with a klass no
Set[1, 2].to_set { … } — Set, with a block no
Set[1, 2].to_set no

So the deprecation is on the Enumerable path when arguments are passed, and Set#to_set — which also accepts a klass — does not carry it.

⛔ I have not established whether a block alone triggers it on the Enumerable path, nor whether *args without a klass does; the table above is what I measured and no more. The rule looks like "arguments passed to the Enumerable version", but one of those rows could contradict it.

Not a regression, and not from #903

rbgo has never emitted this warning. prelude.rb:169 is
Enumerable#to_set(klass = Set, *args, &block) and simply forwards. The
#903 change (a Set
subclass reporting its own class) touched Set#to_set, which is a different
method and does not warn in MRI either — the two are independent and the stdout
of all eight to_set rows agrees.

Why it is worth having

Warning[:deprecated] and -W:deprecated are how a Ruby program finds the
calls it has to change before the next release. A runtime that stays silent
tells its users they have no work to do. The same shape as the
straight-quote message migration in
#900: a message, not a
behaviour, and still a divergence a program can observe.

Scope

A rbWarn at prelude.rb:169's Go-visible counterpart, gated the way rbgo
gates other deprecations — plus whatever the two unmeasured rows above turn out
to require. Worth checking at the same time whether other Enumerable
deprecations of the 4.0 cycle are equally silent here; this one was found by
accident while probing something else, which is not a sampling method.

🤖 Generated with Claude Code

Activity

  1. tannevaled commented on Nov 8, 2026

    @tannevaled
    ContributorAuthor

    The corpus already asserts this, and the example is FAILING — so this is +1 on the ratchet, not cosmetic

    I filed this from a probe and wrote that finding it by accident "is not a sampling method". Correct, and the corpus had it all along: core/enumerable/to_set_spec.rb.

    ruby_version_is "4.0"..."4.1" do
      it "instantiates an object of provided as the first argument set class" do
        set = nil
        proc{set = [1, 2, 3].to_set(EnumerableSpecs::SetSubclass)}.should complain(/Enumerable#to_set/)
        set.should.is_a?(EnumerableSpecs::SetSubclass)
        set.to_a.sort.should == [1, 2, 3]
      end
    end

    Measured on main at 6898f4b4 and on the branch of #910 — identical, so this is independent of both:

    core/enumerable/to_set_spec.rb   pass=2 fail=1
    
    Enumerable#to_set | instantiates an object of provided as the first argument set class
        expected a warning matching /Enumerable#to_set/, got ""
    

    ⭐ And the guard makes it a version-move item

    ruby_version_is "4.0"..."4.1" — the example exists only for 4.0.x. Below 4.0 the same description asserts the call without any warning; at 4.1 the deprecation is gone and so is the example. So this is not a permanent gap to fill at leisure: it is one of the examples the move to RUBY_VERSION 4.0.9 newly admitted, the same shape as everything in the (now closed) #799, and it will stop being assertable when the corpus moves to 4.1.

    ⛔ The complain matcher is NOT stubbed, whatever the README says

    scripts/conformance/rubyspec/README.md lists complain among the matchers that "are stubbed, so those examples count as skipped rather than passing". That is wrong, in a direction that matters:

    • spec_helper.rb:204-249 implements a real ComplainMatcher with a $stderr capture, and :249 wires complain;
    • output (:254-279) and be_computed_by (:199) are implemented too;
    • genuinely stubbed are ruby_exe (:882) and output_to_fd (:280), both raising SpecSkip.

    The failure above is the proof: a skipped example cannot report expected a warning matching /…/, got "".

    It matters because that sentence is load-bearing — cmd/phantom's rationale quotes it to argue "the stubbed matchers make the total too LOW". For complain the error runs the other way: those examples fail and count against the ratchet. I will correct the README separately rather than inside a Set change.

    Scope, revised

    Narrower than I guessed when filing. The spec asserts only that something matching /Enumerable#to_set/ reaches $stderr when arguments are passed to the Enumerable version — which is the one row I had measured as warning. The two rows I flagged as unmeasured (a block alone; *args without a klass) are still unmeasured and the spec does not pin them, so they stay open questions rather than requirements.

    🤖 Generated with Claude Code

  2. tannevaled commented on Nov 8, 2026

    @tannevaled
    ContributorAuthor

    ⛔ It is rb_warn, NOT a :deprecated category warning — measured, and it inverts the obvious fix

    The message says "deprecated", and this repository has warnDeprecated (globals.go:306) which gates on $VERBOSE and Warning[:deprecated]. Using it would be wrong. Measured on 4.0.7:

    condition MRI
    default ($VERBOSE=false, Warning[:deprecated]=false) warns
    Warning[:deprecated] = false warns
    Warning[:deprecated] = true, $VERBOSE = nil silent
    Warning[:deprecated] = true, $VERBOSE = false warns

    So the category is not consulted at all — it warns with Warning[:deprecated] = false — and the only gate is $VERBOSE != nil. That is plain rb_warn (error.c:466), which this repository already has as rbWarn (builtins.go:12734, documented against that exact upstream line).

    ⛔ Why this matters for the spec: core/enumerable/to_set_spec.rb does not enable the category, so a warnDeprecated implementation would stay silent in exactly the case the spec exercises and the example would keep failing while looking fixed. The helper whose name matches the message is the wrong one; the helper whose gating matches is rbWarn.

    Same shape as #906, where respond_to? was the obvious predicate and a method-entry lookup was the correct one — the name suggested one thing and the measurement said another.

    The real work: the method is in Ruby, the helper is in Go

    Enumerable#to_set is prelude.rb:169:

    def to_set(klass = Set, *args, &block)
      klass.new(self, *args, &block)
    end

    rbWarn is called from Go (builtins.go:3152, :4445, …) and has no prelude-visible counterpart. Kernel#warn is not a substitute: MRI's Kernel#warn writes unconditionally, so it would speak when $VERBOSE is nil, where the table above says MRI is silent.

    So the choice is between giving the prelude a $VERBOSE-gated primitive and moving this one method's argument check into Go. Not deciding that here — the measurement is the part that was missing, and it rules out the option someone would otherwise reach for first.

    Still unestablished

    Whether a block alone, or *args without an explicit klass, also warns. The spec pins only the explicit-klass case. Noted when filing and still true.

    🤖 Generated with Claude Code

  3. tannevaled commented on Nov 8, 2026

    @tannevaled
    ContributorAuthor

    The condition, measured exactly — and two constraints that rule out the obvious implementation

    I had left two questions open when filing. Both are answered, and the answers change the fix.

    1. Any POSITIONAL argument warns; a block alone does not

    written MRI 4.0.7
    [1,2].to_set —
    [1,2].to_set { |x| x } — (a block alone does NOT warn)
    [1,2].to_set(Set) warns — although Set is the default
    [1,2].to_set(Sub) warns
    [1,2].to_set(Sub) { |x| x } warns

    The trigger is an argument was passed, not it differs from the default. So the current signature cannot express it:

    def to_set(klass = Set, *args, &block)   # prelude.rb:169

    klass is Set both when omitted and when Set is passed, and only the second warns. The condition needs *args (or a sentinel default).

    2. ⛔ But *args plus a plain super would be WRONG — Range#to_set proves it

    written MRI 4.0.7
    (1..3).to_set —
    (1..3).to_set(Sub) warns, at the caller's line
    {a: 1}.to_set —
    [1,2].lazy.to_set —
    Set[1].to_set(Sub) — (confirms Set#to_set carries no deprecation)

    Range#to_set (prelude.rb:1143) is def to_set(klass = Set, *args, &block) ending in super. With Enumerable#to_set changed to *args, zsuper would forward klass as a bound positional — so (1..3).to_set would warn, where MRI is silent. Changing one signature requires changing its delegator in the same breath.

    3. ⛔ And the warning's location is the USER's frame, through the delegation

    (1..3).to_set(Sub) warns with the caller's file:line (line 8 of my probe), not Range#to_set's line in the prelude. So a prefix built from caller_locations(1, 1) inside Enumerable#to_set is right for a direct call and wrong for one reached via super — it would name the prelude.

    ⭐ caller_locations(1, 1) itself behaves identically in rbgo and MRI (2 engines AGREED, byte for byte), so the prefix is achievable; the frame DEPTH is the part that needs care, not the API.

    4. It is rb_warn, so no new Go API is needed

    Per the gating measured earlier (warns with Warning[:deprecated] = false, silent with $VERBOSE = nil), this is rb_warn: "if $VERBOSE is not nil, write the prefixed message via Warning.warn". The prelude can do that itself — $VERBOSE and Warning.warn (prelude.rb:2750) are both reachable from Ruby — so the choice I posed earlier between a new $VERBOSE-gated primitive and moving the method into Go is a false one. Neither is required.

    Brief

    1. Enumerable#to_set: *args, warn when !args.empty? && !$VERBOSE.nil?, then klass = args.empty? ? Set : args.shift.
    2. Range#to_set: stop forwarding a defaulted klass — forward *args through, so an omitted klass stays omitted.
    3. The prefix must name the original caller, not the delegating prelude frame.
    4. Expected gain: +1 on core/enumerable/to_set_spec.rb, which asserts this with complain(/Enumerable#to_set/).

    Not implementing it in this pass: items 2 and 3 are the kind of frame-depth detail that wants its own change and its own probe rows rather than being appended to a Set branch. The measurements above are the part that was missing.

    🤖 Generated with Claude Code

  4. tannevaled commented on Nov 8, 2026

    @tannevaled
    ContributorAuthor

    Narrowing item 3: the prefix machinery already works — measured

    priorart --repo go-embedded-ruby/ruby "Enumerable to_set deprecation warning" surfaces #699 (open):

    Warnings bypass an overridden Warning.warn, lose their file:lineno prefix, and a redefined constant emits one warning where MRI emits two

    That reads as a blocker for the brief above, which needs the warning to carry the caller's file:line. It is not. Measured on main at c604cce1, using an existing rbWarn site (Array.new(2, 0) { |i| i }):

    ruby@4.0  …/warnpfx2.rb:1: warning: block supersedes default value argument
    rbgo      …/warnpfx2.rb:1: warning: block supersedes default value argument
    
    === 2 engines AGREED, byte for byte ===
    

    So the prefix half of #699 is satisfied today — consistent with #706 (merged 2026-09-26) having referenced it. #699 stays open on its other claims, which I have not re-measured and am not pronouncing on.

    ⛔ Which means item 3 of the brief is narrower than I wrote it. It is not "the prefix mechanism needs work": rbWarn produces MRI's exact prefix from the Go side. The only open question is which frame the prelude asks for when Enumerable#to_set is reached through Range#to_set's super — a caller_locations depth, not a missing capability.

    ⛔ And a note on my first attempt at this measurement, because it would have been a wrong answer: I probed with Hash.new(0) { |x| 1 }, expecting a warning. Both engines raise ArgumentError there (4.0 forbids a default and a block), so I had measured an exception and not a warning at all. The second probe used a site the source shows calling rbWarn (builtins.go:3152). Picking the trigger from the code rather than from memory is what made the comparison mean anything.

    🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions