debug icon indicating copy to clipboard operation
debug copied to clipboard

Fix incorrect `#bind_call` monkey patch

Open amomchilov opened this issue 2 years ago • 10 comments

Description

Ruby/debug monkey patches in an implementation of UnboundMethod#bind_call for compatibility with Rubies older than 2.7. Unfortunately, there is a bug in the implementation, in that it doesn't pass the block parameter correctly. Here's a demo:

$ chruby 2.6.10 && ruby ./bind_call_bug_demo.rb
Backporting UnboundMethod#bind_call in Ruby 2.6.10.

Incorrect result from buggy bind_call: nil
  Correct result from fixed bind_call: #<Proc:0x00000001100df230@./bind_call_bug_demo.rb:12>
                        Expected proc: #<Proc:0x00000001100df230@./bind_call_bug_demo.rb:12>

$ chruby 3.2.2 && ruby ./bind_call_bug_demo.rb
Ruby 3.2.2 already has UnboundMethod#bind_call natively defined.

Correct result from native bind_call: #<Proc:0x00000001055ab5f8 ./bind_call_bug_demo.rb:12>
                       Expected proc: #<Proc:0x00000001055ab5f8 ./bind_call_bug_demo.rb:12>
bind_call_bug_demo.rb
require "test/unit"

class SampleTarget < Struct.new(:id)
  def pass_block_through(&block)
    block
  end
end

target_object = SampleTarget.new

m = SampleTarget.instance_method(:pass_block_through)
proc = Proc.new { |x| x }


if UnboundMethod.method_defined?(:bind_call)
  puts "Ruby #{RUBY_VERSION} already has UnboundMethod#bind_call natively defined.\n\n"

  puts "Correct result from native bind_call: #{m.bind_call(target_object, &proc).inspect}" # => #<Proc:...>
  puts "                       Expected proc: #{proc}"  # => #<Proc:...>
else
  puts "Backporting UnboundMethod#bind_call in Ruby #{RUBY_VERSION}.\n\n"

  class UnboundMethod
    def bind_call(receiver, *args)
      bind(receiver).call(*args)
    end

    def bind_call_fixed(receiver, *args, &block)
      bind(receiver).call(*args, &block)
    end
  end

  puts "Incorrect result from buggy bind_call: #{m.bind_call(target_object, &proc).inspect}" # => nil
  puts "  Correct result from fixed bind_call: #{m.bind_call_fixed(target_object, &proc).inspect}" # => #<Proc:...>
  puts "                        Expected proc: #{proc}"  # => #<Proc:...>
end

This repo introduces a corrected patch, and with a test that confirms it:

# for Ruby 2.6 compatibility
unless UnboundMethod.method_defined?(:bind_call)
  class UnboundMethod
    def bind_call(receiver, *args, &block)
      bind(receiver).call(*args, &block)
    end
  end
end

I also introduces a new Reflection module which extracts all the bind_call-ed reflection methods, so they're more readable. It also adds tests for them.

amomchilov avatar Jul 27 '23 19:07 amomchilov

The name "Reflection" is not clear for me. Reflection is important programming technique and debugger should support it especially on Ruby. However, this module doesn't represent this kind of general idea. Could you make another word?

ko1 avatar Sep 25 '23 07:09 ko1

@ko1 I think Reflection is exactly the right term. Have a look at the Wikiepdia page:

  • Discover and modify source-code constructions (such as code blocks, classes, methods, protocols, etc.) as first-class objects at runtime.
  • Convert a string matching the symbolic name of a class or function into a reference to or invocation of that class or function.

amomchilov avatar Sep 26 '23 22:09 amomchilov

Yes, this module uses "Reflection" feature, but this module doesn't represent the feature of "Reflection".

ko1 avatar Sep 27 '23 01:09 ko1

@ko1 Sorry, I don't think I understand.

I suppose ReflectionHelpers/ReflectionUtils would be more precise names, but *Helpers names are smelly. It reads better at the callsite, e.g. the standard library gives you Math.sin not MathHelpers.sin.

This naming also what Tapioca calls it.

If you would like an alternative name, could you suggest one? I don't have any better ideas.

amomchilov avatar Sep 27 '23 17:09 amomchilov

If you would like an alternative name, could you suggest one? I don't have any better ideas.

This module provides a feature to call essential Ruby methods so

  • EssentialRubyFeature
  • PrimitiveRubyFeature
  • OriginalRubyFeature

and so on...?

ko1 avatar Oct 02 '23 07:10 ko1

Personally, I think those are too generic.

___RubyFeature raises the question: "Which Ruby features are essential, primitive or original?"

The answer is: "the reflection-related features"

I really do think Reflection is the exact right term for this, but it's more important to me that this lands in any form. Please feel free to use whichever you find best.

amomchilov avatar Oct 02 '23 14:10 amomchilov

___RubyFeature raises the question: "Which Ruby features are essential, primitive or original?" The answer is: "the reflection-related features"

I understand the misunderstanding with us.

  • You understand these methods are "reflection related features"
  • I understand these methods are guaranteed to be original methods.

Original methods mean that they are guaranteed to provide original feature even if they are re-defined by other classes.

For example, if we get trouble with Array#each redefinition, I think Array.instance_method(:each) should be in this module. But maybe you don't think so.

Hmm.

ko1 avatar Oct 09 '23 08:10 ko1

Hi Koichi! You're correct, I think this is exactly the misunderstanding.

For example, if we get trouble with Array#each redefinition, I think Array.instance_method(:each) should be in this module. But maybe you don't think so.

Correct! I was thinking of it more by purpose ("reflection related features") than implementation detail ("it's an original Ruby method, ignoring redefinition"). If I wanted the original Array#each, I would put it in some new array-specific module which better represents some relevant high-level idea (e.g. “Enumeration”)

I'm okay with either I approach. Please feel free to merge whichever you prefer.

amomchilov avatar Oct 10 '23 00:10 amomchilov

I also misunderstand that you introduced different naming like

    module_function def instance_variable_get_from(o, name)
      M_INSTANCE_VARIABLE_GET.bind_call(o, name)
    end

(not instance_variable_get).

ko1 avatar Oct 19 '23 09:10 ko1

OK, How about ObjectIntrospector?

@mametter told me that https://stackoverflow.com/questions/25198271/what-is-the-difference-between-introspection-and-reflection#answer-41161878

here we need the ability for "Introspection"

ko1 avatar Oct 19 '23 09:10 ko1