Project

General

Profile

Actions

Bug #22058

open

{Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope

Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope

Added by jeremyevans0 (Jeremy Evans) 3 days ago. Updated about 20 hours ago.

Status:
Open
Assignee:
-
Target version:
-
ruby -v:
ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8]
[ruby-core:125428]

Description

I've found that Method#super_method and InstanceMethod#super_method do not work correctly in some cases for refined methods. At the least, there is a definite bug, which is that #super_method inside a scope with a refinement activated for the method results in a loop over the refined methods.

I'm not sure if the semantics for #super_method for refined methods were ever discussed. Other than the bug regarding the loop over the refinement, the currently semantics seem to be:

  • While handling additional refined methods for the same class as the current method, #super_method will consider the refinements active at the point the method was created
  • For ancestors, #super_method will consider the refinements active in the scope calling #super_method

These semantics seem questionable. My guess is they are not the result of intentional design, but due purely to implementation details. I see two possibilities:

  1. Keep the current behavior, where results depend on refinements activated in the caller's namespace. In this case, I recommend that we not have different handling when there are other refinements for the same class as the current method should. If refinements are not in scope, #super_method should not return them. We should also consider whether #super_method should error if the receiver is a refinement method for a refinement not activated in the caller's scope.

  2. Change #super_method so that it depends on the refinements activated in the namespace it was created in, for ancestors as well as for the current class. This would make #super_method return the same result no matter where it was called.

I think option 2 makes more sense, but it requires that Method/InstanceMethod objects for refined methods keep a reference to the scope in which they were created.

A related minor bug I found during this research is the #inspect output for #super_method results for refined methods also does not use the same format, indicating there is something internally different.

Here's example code showing these issues, with commented output below:

class B
  def b = 16
end

class A < B
  def b = 8 + super
end

module M1
  refine(A){def b = 1 + super}
end

module M2
  refine(A){def b = 2 + super}
end

module M3
  refine(A){def b = 4 + super}
end

module M4
  refine(B){def b = 4 + super}
end

I = A.new.method(:b)
C = A.instance_method(:b)

module N0
  using M1
  using M3
  I = A.new.method(:b)
  C = A.instance_method(:b)
end

module N
  using M1
  using M2
  using M3
  using M4

  I = A.new.method(:b)
  C = A.instance_method(:b)

  puts "", "Inside module using all refinements using method created using all refinements:"
  p I
  i = I
  6.times do
    i = i.super_method
    p i
  end

  puts

  p C
  c = C
  6.times do
    c = c.super_method
    p c
  end

  puts "", "Inside module using all refinements using method created using some refinements:"
  p N0::I
  i = N0::I
  6.times do
    i = i.super_method
    p i
  end

  puts

  p N0::C
  c = N0::C
  6.times do
    c = c.super_method
    p c
  end

  puts "", "Inside module using all refinements using method created using no refinements:"
  p ::I
  i = ::I
  6.times do
    break unless i = i.super_method
    p i
  end

  puts

  p ::C
  c = ::C
  6.times do
    break unless c = c.super_method
    p c
  end
end

module N0
  using M1
  using M3

  puts "", "Inside module using some refinements using method created using all refinements:"

  p N::I
  i = N::I
  6.times do
    break unless i = i.super_method
    p i
  end

  puts

  p N::C
  c = N::C
  6.times do
    break unless c = c.super_method
    p c
  end
end

puts "", "Top level using method created with all refinements:"

p N::I
i = N::I
6.times do
  break unless i = i.super_method
  p i
end

puts

p N::C
c = N::C
6.times do
  break unless c = c.super_method
  p c
end

puts "", "Top level using method created with some refinements:"

p N0::I
i = N0::I
6.times do
  break unless i = i.super_method
  p i
end

puts

p N0::C
c = N0::C
6.times do
  break unless c = c.super_method
  p c
end

puts "", "Top level using method created with no refinements:"

p I
i = I
6.times do
  break unless i = i.super_method
  p i
end

puts

p C
c = C
6.times do
  break unless c = c.super_method
  p c
end

First, the bug. This shows a loop among active refinements. After going through M3, M2, and M1, it loops back to M3. This also shows weird #inspect output, with #super_method not showing the main class.

Inside module using all refinements using method created using all refinements:
#<Method: A(#<refinement:A@M3>)#b() t/t43.rb:42>
#<Method: #<refinement:A@M2>#b() t/t43.rb:38>
#<Method: #<refinement:A@M1>#b() t/t43.rb:34>
#<Method: #<refinement:A@M3>#b() t/t43.rb:42>
#<Method: #<refinement:A@M2>#b() t/t43.rb:38>
#<Method: #<refinement:A@M1>#b() t/t43.rb:34>
#<Method: #<refinement:A@M3>#b() t/t43.rb:42>

#<UnboundMethod: #<refinement:A@M3>#b() t/t43.rb:42>
#<UnboundMethod: #<refinement:A@M2>#b() t/t43.rb:38>
#<UnboundMethod: #<refinement:A@M1>#b() t/t43.rb:34>
#<UnboundMethod: #<refinement:A@M3>#b() t/t43.rb:42>
#<UnboundMethod: #<refinement:A@M2>#b() t/t43.rb:38>
#<UnboundMethod: #<refinement:A@M1>#b() t/t43.rb:34>
#<UnboundMethod: #<refinement:A@M3>#b() t/t43.rb:42>

This shows the dynamic scoping of the #super_method. At time of call, only M3 and M1 are in scope. You see in the first loop, only M3 and M1 are used, but subsequent loops use M3, M2, and M1.

Inside module using all refinements using method created using some refinements:
#<Method: A(#<refinement:A@M3>)#b() t/t43.rb:42>
#<Method: #<refinement:A@M1>#b() t/t43.rb:34>
#<Method: #<refinement:A@M3>#b() t/t43.rb:42>
#<Method: #<refinement:A@M2>#b() t/t43.rb:38>
#<Method: #<refinement:A@M1>#b() t/t43.rb:34>
#<Method: #<refinement:A@M3>#b() t/t43.rb:42>
#<Method: #<refinement:A@M2>#b() t/t43.rb:38>

#<UnboundMethod: #<refinement:A@M3>#b() t/t43.rb:42>
#<UnboundMethod: #<refinement:A@M1>#b() t/t43.rb:34>
#<UnboundMethod: #<refinement:A@M3>#b() t/t43.rb:42>
#<UnboundMethod: #<refinement:A@M2>#b() t/t43.rb:38>
#<UnboundMethod: #<refinement:A@M1>#b() t/t43.rb:34>
#<UnboundMethod: #<refinement:A@M3>#b() t/t43.rb:42>
#<UnboundMethod: #<refinement:A@M2>#b() t/t43.rb:38>

This shows a case where the Method was created with no refinements activated. Refinements for superclasses that are active in the current scope are still picked up:

Inside module using all refinements using method created using no refinements:
#<Method: A#b() t/t43.rb:30>
#<Method: #<refinement:B@M4>#b() t/t43.rb:46>
#<Method: #<refinement:B@M4>#b() t/t43.rb:46>
#<Method: #<refinement:B@M4>#b() t/t43.rb:46>
#<Method: #<refinement:B@M4>#b() t/t43.rb:46>
#<Method: #<refinement:B@M4>#b() t/t43.rb:46>
#<Method: #<refinement:B@M4>#b() t/t43.rb:46>

#<UnboundMethod: A#b() t/t43.rb:30>
#<UnboundMethod: #<refinement:B@M4>#b() t/t43.rb:46>
#<UnboundMethod: #<refinement:B@M4>#b() t/t43.rb:46>
#<UnboundMethod: #<refinement:B@M4>#b() t/t43.rb:46>
#<UnboundMethod: #<refinement:B@M4>#b() t/t43.rb:46>
#<UnboundMethod: #<refinement:B@M4>#b() t/t43.rb:46>
#<UnboundMethod: #<refinement:B@M4>#b() t/t43.rb:46>

More evidence for dynamic scoping. This is the opposite of the second example, where the first loop has M3, M2, and M1, and subsequent loops have M3 and M1.

Inside module using some refinements using method created using all refinements:
#<Method: A(#<refinement:A@M3>)#b() t/t43.rb:42>
#<Method: #<refinement:A@M2>#b() t/t43.rb:38>
#<Method: #<refinement:A@M1>#b() t/t43.rb:34>
#<Method: #<refinement:A@M3>#b() t/t43.rb:42>
#<Method: #<refinement:A@M1>#b() t/t43.rb:34>
#<Method: #<refinement:A@M3>#b() t/t43.rb:42>
#<Method: #<refinement:A@M1>#b() t/t43.rb:34>

#<UnboundMethod: #<refinement:A@M3>#b() t/t43.rb:42>
#<UnboundMethod: #<refinement:A@M2>#b() t/t43.rb:38>
#<UnboundMethod: #<refinement:A@M1>#b() t/t43.rb:34>
#<UnboundMethod: #<refinement:A@M3>#b() t/t43.rb:42>
#<UnboundMethod: #<refinement:A@M1>#b() t/t43.rb:34>
#<UnboundMethod: #<refinement:A@M3>#b() t/t43.rb:42>
#<UnboundMethod: #<refinement:A@M1>#b() t/t43.rb:34>

This shows the behavior when no refinements are activated in the current scope. It still includes refinements for the same class as the current method, but no refinements for superclasses. Also, note that there is no longer a loop, because there are no refinements activated:

Top level using method created with all refinements:
#<Method: A(#<refinement:A@M3>)#b() t/t43.rb:42>
#<Method: #<refinement:A@M2>#b() t/t43.rb:38>
#<Method: #<refinement:A@M1>#b() t/t43.rb:34>
#<Method: A#b() t/t43.rb:30>
#<Method: B#b() t/t43.rb:26>

#<UnboundMethod: #<refinement:A@M3>#b() t/t43.rb:42>
#<UnboundMethod: #<refinement:A@M2>#b() t/t43.rb:38>
#<UnboundMethod: #<refinement:A@M1>#b() t/t43.rb:34>
#<UnboundMethod: A#b() t/t43.rb:30>
#<UnboundMethod: B#b() t/t43.rb:26>

This shows that only the refinements were activated at the point the method was created are included.

Top level using method created with some refinements:
#<Method: A(#<refinement:A@M3>)#b() t/t43.rb:42>
#<Method: #<refinement:A@M1>#b() t/t43.rb:34>
#<Method: A#b() t/t43.rb:30>
#<Method: B#b() t/t43.rb:26>

#<UnboundMethod: #<refinement:A@M3>#b() t/t43.rb:42>
#<UnboundMethod: #<refinement:A@M1>#b() t/t43.rb:34>
#<UnboundMethod: A#b() t/t43.rb:30>
#<UnboundMethod: B#b() t/t43.rb:26>

This output doesn't show any problems, it shows that if refinements were not activated when the method was created, and are not activated when #super_method is called, that #super_method ignores refinements, which is what you would expect.

Top level using method created with no refinements:
#<Method: A#b() t/t43.rb:30>
#<Method: B#b() t/t43.rb:26>

#<UnboundMethod: A#b() t/t43.rb:30>
#<UnboundMethod: B#b() t/t43.rb:26>
Actions

Also available in: PDF Atom