[ruby-core:125428] [Ruby Bug#22058] {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope
Issue #22058 has been reported by jeremyevans0 (Jeremy Evans). ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058 * Author: jeremyevans0 (Jeremy Evans) * Status: Open * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by Eregon (Benoit Daloze). jeremyevans0 (Jeremy Evans) wrote:
I'm not sure if the semantics for #super\_method for refined methods were ever discussed.
I recall https://bugs.ruby-lang.org/issues/17007 (loop with `super` and refinements). That was solved by preventing `include/prepend` in a Refinement, so probably not quite the same case but sounds related. ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117238 * Author: jeremyevans0 (Jeremy Evans) * Status: Open * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by shugo (Shugo Maeda). jeremyevans0 (Jeremy Evans) wrote:
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.
Agreed. FYI, I tried using rb_callable_method_entry_without_refinements() in method_super_method() to avoid the infinite loop, but it returns a wrong result in the following case: ```ruby class Base def foo; "Base"; end end class Child < Base; end module N refine Base do def foo; "N-Base"; end end end using N module M refine Child do def foo; "M-Child:" + super; end end end using M p Child.new.foo #=> "M-Child:N-Base" m = Child.new.method(:foo) p m.super_method.owner #=> Base (should be #<refinement:Base@N>) p m.super_method.call #=> "Base" (should be "N-Base") ``` ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117280 * Author: jeremyevans0 (Jeremy Evans) * Status: Open * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by shugo (Shugo Maeda). shugo (Shugo Maeda) wrote in #note-2:
FYI, I tried using rb_callable_method_entry_without_refinements() in method_super_method() to avoid the infinite loop, but it returns a wrong result in the following case:
Actually, I need to revise my previous comment. Looking at doc/syntax/refinements.rdoc:
Note that +super+ in a method of a refinement invokes the method in the refined class even if there is another refinement which has been activated in the same context.
The expected result of Child.new.foo in my example should be "M-Child:Base", not "M-Child:N-Base". The current super behavior appears to be a bug introduced as an unintended side effect of the fix for Bug #16107 (commit 11a9f7ab94), which changed search_refined_method to iterate all CREFs instead of only the first one. The fix was intended to make refined methods see refinements activated via using inside the refine block, but it inadvertently changed super behavior as well. If this is correct, rb_callable_method_entry_without_refinements() gives the right result for Method#super_method, and would also be consistent with fixing the super behavior itself. ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117292 * Author: jeremyevans0 (Jeremy Evans) * Status: Open * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by matz (Yukihiro Matsumoto). @shugo is right. The change in super's behavior in 2.7 was unintentional, so I'm in favor of fixing it to match the documentation. Matz. ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117296 * Author: jeremyevans0 (Jeremy Evans) * Status: Open * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by shugo (Shugo Maeda). matz (Yukihiro Matsumoto) wrote in #note-4:
@shugo is right. The change in super's behavior in 2.7 was unintentional, so I'm in favor of fixing it to match the documentation.
I realized that completely ignoring refinements when searching via super might be problematic. As shown in the example of [Bug #13227], there is a use case where one wants super to search refinements when multiple classes in an inheritance relationship are refined within the same module. We have three options: 1. Keep the current behavior of super as-is. 2. Change super to search refinements only in the same module. 3. Change super to ignore refinements completely. If we choose option 1 or 2, Method#super_method has to be fixed as Jeremy suggested. I think option 3 may break existing programs. ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117302 * Author: jeremyevans0 (Jeremy Evans) * Status: Assigned * Assignee: shugo (Shugo Maeda) * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by shugo (Shugo Maeda). jeremyevans0 (Jeremy Evans) wrote:
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
Could you clarify what you mean by "the scope in which they were created"? To make `Method#super_method` consistent with the actual behavior of `super` in the method body, I think we should use the CREF where the method was *defined*, not the one where `Kernel#method` was invoked. If that's correct, we can use `me->def->body.iseq.cref` directly, so there's no need to keep additional information on the Method/InstanceMethod object. Am I missing something? ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117322 * Author: jeremyevans0 (Jeremy Evans) * Status: Assigned * Assignee: shugo (Shugo Maeda) * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by jeremyevans0 (Jeremy Evans). shugo (Shugo Maeda) wrote in #note-7:
jeremyevans0 (Jeremy Evans) wrote:
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
Could you clarify what you mean by "the scope in which they were created"?
As you mention below, this would be the CREF of the scope in which `Kernel#method` was invoked.
To make `Method#super_method` consistent with the actual behavior of `super` in the method body, I think we should use the CREF where the method was *defined*, not the one where `Kernel#method` was invoked.
If that's correct, we can use `me->def->body.iseq.cref` directly, so there's no need to keep additional information on the Method/UnboundMethod object. Am I missing something?
Assuming we are going to use a CREF to consider refinements during `super` method lookup, using the CREF of the refinement method definition for would result in incorrect behavior, I think. We want `super_method` to return the method that `super` would call (and for `super_method` of that method to work similarly for all super methods). In order for that to be possible, I think we need the CREF of the caller of `Kernel#method`, not the one of the method definition, as the refinements in scope could be different. Note that doc/syntax/refinements.rdoc and https://github.com/ruby/ruby/wiki/Refinements-Spec both imply that refinements for superclasses will be respected during `super` method lookup, which is not what currently happens. Currently, `super` inside a refined method appears to ignore any further refinements, not just for the current class, but for superclasses as well. Example: ```ruby module C def a; "C" + super end end module D def a; "D" + super end end module E def a; "E" + super end end class A prepend D def a; "A" end end class B < A include C prepend E def a; "B"+super end end module M refine A do def a; "M"+super end end end module N refine B do def a; "N"+super end end end module O refine A do def a; "O"+super end end end module P refine B do def a; "P"+super end end end p "No refinements: #{B.new.a}" module T using M using O p "Refine A: #{B.new.a}" end module U using N using P p "Refine B: #{B.new.a}" end module V using O using P using M using N p "Refine A, B, R: #{B.new.a}" end ``` Output: ``` "No refinements: EBCDA" "Refine A: EBCDA" "Refine B: PEBCDA" "Refine A, B, R: NEBCDA" ``` ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117326 * Author: jeremyevans0 (Jeremy Evans) * Status: Assigned * Assignee: shugo (Shugo Maeda) * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by shugo (Shugo Maeda). jeremyevans0 (Jeremy Evans) wrote in #note-8:
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
Could you clarify what you mean by "the scope in which they were created"?
As you mention below, this would be the CREF of the scope in which `Kernel#method` was invoked.
`Kernel#method` can be invoked anywhere, including in a context that is irrelevant to the method definition, so using the caller's CREF could be problematic.
To make `Method#super_method` consistent with the actual behavior of `super` in the method body, I think we should use the CREF where the method was *defined*, not the one where `Kernel#method` was invoked.
If that's correct, we can use `me->def->body.iseq.cref` directly, so there's no need to keep additional information on the Method/UnboundMethod object. Am I missing something?
Assuming we are going to use a CREF to consider refinements during `super` method lookup, using the CREF of the refinement method definition for would result in incorrect behavior, I think. We want `super_method` to return the method that `super` would call (and for `super_method` of that method to work similarly for all super methods). In order for that to be possible, I think we need the CREF of the caller of `Kernel#method`, not the one of the method definition, as the refinements in scope could be different.
Note that doc/syntax/refinements.rdoc and https://github.com/ruby/ruby/wiki/Refinements-Spec both imply that refinements for superclasses will be respected during `super` method lookup, which is not what currently happens. Currently, `super` inside a refined method appears to ignore any further refinements, not just for the current class, but for superclasses as well. Example:
The documentation may be misleading, but in my view the refinements activated on the caller's side should not affect `super`, regardless of whether they are for the current class or for the superclass. This is true both in the current behavior and in options 2 and 3 in #note-6, so we don't need to keep the caller's CREF. In the initial implementation, refinements for the same refined class activated on the caller's side did affect `super`, but the behavior was later changed to make `super` resolution with refinements more static. That's why the following note was added:
Note that +super+ in a method of a refinement invokes the method in the refined class even if there is another refinement which has been activated in the same context.
Module inclusion into refinements is no longer possible, so the `+super+` section can be simplified to describe the current behavior as follows:
== +super+
When +super+ is invoked, method lookup starts from the superclass of the current class (or, for a method in a refinement, from the refined class itself), and proceeds as described in the Method Lookup section above.
Refinements activated at the call site of a refinement method do not affect +super+ inside that method; only refinements that were in scope at the point of the method definition can affect the +super+ method lookup.
---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117330 * Author: jeremyevans0 (Jeremy Evans) * Status: Assigned * Assignee: shugo (Shugo Maeda) * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by jeremyevans0 (Jeremy Evans). @shugo Thank you, I think I finally understand. After the first refinement method is called, the caller's scope no longer matters, because each `super` call site considers the refinements in effect at that `super` call site. You cannot use refinements to insert into the middle of a method lookup chain, only to insert at the start of a method lookup chain, unless you control the `super` call sites. I submitted a documentation pull request for doc/syntax/refinements.rdoc yesterday: https://github.com/ruby/ruby/pull/16972. I've updated it today based on this discussion, and also to fix some other issues with the documentation. I'll work on a fix for `super_method`. ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117341 * Author: jeremyevans0 (Jeremy Evans) * Status: Assigned * Assignee: shugo (Shugo Maeda) * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by shugo (Shugo Maeda). jeremyevans0 (Jeremy Evans) wrote in #note-10:
@shugo Thank you, I think I finally understand. After the first refinement method is called, the caller's scope no longer matters, because each `super` call site considers the refinements in effect at that `super` call site. You cannot use refinements to insert into the middle of a method lookup chain, only to insert at the start of a method lookup chain, unless you control the `super` call sites.
I submitted a documentation pull request for doc/syntax/refinements.rdoc yesterday: https://github.com/ruby/ruby/pull/16972. I've updated it today based on this discussion, and also to fix some other issues with the documentation.
It looks good to me. Thank you! ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117342 * Author: jeremyevans0 (Jeremy Evans) * Status: Assigned * Assignee: shugo (Shugo Maeda) * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by jeremyevans0 (Jeremy Evans). It took a lot of work, but I was able to fix `Method#super_method` to return the same method that `super` would call when refinements are used: https://github.com/ruby/ruby/pull/16997 I'm not positive it handles every case correctly, but it handles every case I've tried correctly. If anyone wants to add test cases, I think that would be great. Even if they don't requires code changes to pass, as long as they test a case not currently tested, I think that would be valuable. ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117348 * Author: jeremyevans0 (Jeremy Evans) * Status: Assigned * Assignee: shugo (Shugo Maeda) * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by jeremyevans0 (Jeremy Evans). I merged commit:cb0cd76a086d2d9423adaa4c5032a5fa149e7ef1 and commit:8f8dd01a93b247f607d927770a2ad09e1e0a1647 to handle address the issues identified in this bug report. I didn't have them auto-close this report, because there is still a question of whether `super` in a refinement method should be allowed to call another refinement method for the same base method, if the additional refinement is activated during the `super` call. Example: ```ruby class A def b; "A" end end module M R = refine(A) { def b; "M" + super; end } end module N using M R = refine(A) { def b; "N" + super; end } end using M using N A.new.b # 2.1-2.6: "NA" # 2.7-4.0: "NMA" ``` I think the 2.7+ behavior makes more sense, so I would be in favor of keeping it, and updating the documentation. ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117369 * Author: jeremyevans0 (Jeremy Evans) * Status: Assigned * Assignee: shugo (Shugo Maeda) * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
Issue #22058 has been updated by shugo (Shugo Maeda). jeremyevans0 (Jeremy Evans) wrote in #note-13:
```ruby class A def b; "A" end end module M R = refine(A) { def b; "M" + super; end } end module N using M R = refine(A) { def b; "N" + super; end } end using M using N A.new.b # 2.1-2.6: "NA" # 2.7-4.0: "NMA" ```
I think the 2.7+ behavior makes more sense, so I would be in favor of keeping it, and updating the documentation.
Initially I thought the pre-2.7 behavior was more reasonable, but I now agree that the 2.7+ behavior is more consistent. I'd like to confirm this with Matz at the next developer meeting. ---------------------------------------- Bug #22058: {Method,InstanceMethod}#super_method doesn't work correctly for refined method with refinements for method active in the caller's scope https://bugs.ruby-lang.org/issues/22058#change-117370 * Author: jeremyevans0 (Jeremy Evans) * Status: Assigned * Assignee: shugo (Shugo Maeda) * ruby -v: ruby 4.1.0dev (2026-05-03T23:57:21Z master d8d2ed5dc9) +PRISM [x86_64-openbsd7.8] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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: ```ruby 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> ``` -- https://bugs.ruby-lang.org/
participants (4)
-
Eregon (Benoit Daloze) -
jeremyevans0 (Jeremy Evans) -
matz (Yukihiro Matsumoto) -
shugo (Shugo Maeda)