Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JRuby 9.1.17.0 fails when using 'contracts' gem #5155

Open
Confusion opened this issue Apr 28, 2018 · 7 comments
Open

JRuby 9.1.17.0 fails when using 'contracts' gem #5155

Confusion opened this issue Apr 28, 2018 · 7 comments

Comments

@Confusion
Copy link

Confusion commented Apr 28, 2018

Code

require 'contracts'
include Contracts::Core
class C end

Expected Behavior

The script executes successfully with exit code 0

Actual Behavior

NoMethodError: undefined method `owner_class=' for #<Contracts::Engine::Base:0x3ffcd140 @klass=#<Class:C>>
  set_eigenclass_owner at /home/wever/.rvm/gems/jruby-9.1.17.0@rails/gems/contracts-0.16.0/lib/contracts/engine/base.rb:48
             inherited at /home/wever/.rvm/gems/jruby-9.1.17.0@rails/gems/contracts-0.16.0/lib/contracts/decorators.rb:8
                <main> at demo.rb:3

The contracts gem performs some interesting Ruby voodoo :)

Environment

jruby 9.1.17.0 (2.3.3) 2018-04-20 d8b1ff9 OpenJDK 64-Bit Server VM 25.162-b12 on 1.8.0_162-8u162-b12-0ubuntu0.16.04.2-b12 [linux-x86_64]

Linux photon 4.4.0-119-generic #143-Ubuntu SMP Mon Apr 2 16:08:24 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

Only gem: contracts 0.16.0

JRuby 9.1.8.0 shows the same behavior.

@Confusion Confusion changed the title JRuby 9.1.17.0 (and earlier) fails when using 'contracts' gem JRuby 9.1.17.0 fails when using 'contracts' gem Apr 28, 2018
@Confusion
Copy link
Author

Confusion commented May 1, 2018

So I've reduced the issue to a self contained script:

module InheritHook
  def inherited(subclass)
    puts "InhertHook#inherited called for #{subclass}"
    subclass.base_or_eigenclass_instance.set_eigenclass_owner
  end
end

class Base
  def initialize(klass)
    puts "#{self.class}#initialize called for #{klass}"
    @klass = klass
    puts "Returning #{self}"
  end

  def set_eigenclass_owner
    puts "Base#set_eigenclass_owner called on #{self}"
    @klass.singleton_class.base_or_eigenclass_instance.owner_class = 'something'
  end
end

class EigenClass < Base
  attr_writer :owner_class
end

module Engine
  def self.apply(target, klass = Base)
    puts "Engine.apply called on #{target}, #{klass}"
    return if target.respond_to?(:base_or_eigenclass_instance)

    apply_to_eigenclass(target)

    target.instance_eval do
      define_method(:base_or_eigenclass_instance) do
        @base_or_eigenclass_instance ||= klass.new(self)
      end
    end
  end

  private

  def self.apply_to_eigenclass(target)
    puts "Engine.apply_to_eigenclass called on #{target}"
    return if (target <= Object.singleton_class)

    singleton_class = target.singleton_class
    singleton_class.extend(InheritHook)
    apply(singleton_class, EigenClass)
    apply(singleton_class)
  end
end

Object.extend(InheritHook)
Engine.apply(Object)

class C end

Executing with Ruby 2.5 yields e.g.

Engine.apply called on Object, Base
Engine.apply_to_eigenclass called on Object
Engine.apply called on #<Class:Object>, EigenClass
Engine.apply_to_eigenclass called on #<Class:Object>
Engine.apply called on #<Class:Object>, Base
InhertHook#inherited called for C
EigenClass#initialize called for C
Returning #<EigenClass:0x00000001778b88>
Base#set_eigenclass_owner called on #<EigenClass:0x00000001778b88>
EigenClass#initialize called for #<Class:C>
Returning #<EigenClass:0x000000017788b8>

JRuby 9.1.17.0 on the other hand returns

Engine.apply called on Object, Base
Engine.apply_to_eigenclass called on Object
Engine.apply called on #<Class:Object>, EigenClass
Engine.apply_to_eigenclass called on #<Class:Object>
Engine.apply called on #<Class:Object>, Base
Engine.apply_to_eigenclass called on #<Class:Object>
InhertHook#inherited called for C
Base#initialize called for C
Returning #<Base:0x13eb8acf>
Base#set_eigenclass_owner called on #<Base:0x13eb8acf>
Base#initialize called for #<Class:C>
Returning #<Base:0x51c8530f>
NoMethodError: undefined method `owner_class=' for #<Base:0x51c8530f @klass=#<Class:C>>
  set_eigenclass_owner at foo.rb:17
             inherited at foo.rb:4
                <main> at foo.rb:55

So there are obvious differences, but I haven't yet narrowed it down further and thought I'd share my work before someone else went through the same kind of trouble

Moving the singleton_class.extend(InheritHook) in Engine.apply_to_eigenclass down a line (or two) makes the difference disappear by changing the JRuby output to match the MRI output

@Confusion
Copy link
Author

Somewhat smaller variation that no longer raises, but whose output still shows difference between mri and jruby

module InheritHook
  def inherited(subclass)
    puts "InhertHook#inherited(#{subclass})"
    subclass.base_or_eigenclass_instance
  end
end

class Base
  def initialize(klass)
    puts "#{self.class}#initialize(#{klass})"
    @klass = klass
  end
end

class EigenClass < Base
end

module Engine
  def self.apply(target, klass = Base)
    puts "Engine.apply(#{target}, #{klass})"
    if target.respond_to?(:base_or_eigenclass_instance)
      puts "Early return because already applied"
      return
    end

    apply_to_eigenclass(target)

    target.instance_eval do
      define_method(:base_or_eigenclass_instance) do
        @base_or_eigenclass_instance ||= klass.new(self)
      end
    end
  end

  private

  def self.apply_to_eigenclass(target)
    puts "Engine.apply_to_eigenclass(#{target})"
    return if (target <= Object.singleton_class)

    singleton_class = target.singleton_class
    singleton_class.extend(InheritHook)
    apply(singleton_class, EigenClass)
    apply(singleton_class)
  end
end

Object.extend(InheritHook)
Engine.apply(Object)

class C end

Note there are two separate, but probably related, parts:

  • if you comment the last line (class C end', there is already a difference in the output
  • if you include that line, there is a further difference

@Confusion
Copy link
Author

Confusion commented May 2, 2018

A further self contained reduction for the first bullet of the previous comment. Almost all changes now change the result and makes the output for mri and jruby equal

module ExtendedModule end

klass = Object.singleton_class
klass.extend(ExtendedModule)

if !klass.respond_to?(:dynamically_defined_method)
  puts "Defining dynamically_defined_method"

  klass.instance_eval do
    define_method(:dynamically_defined_method) {}
  end
end

puts klass.respond_to?(:dynamically_defined_method)

MRI

Defining dynamically_defined_method
true

JRuby

Defining dynamically_defined_method
false

Especially note that the InheritHook doesn't do anything, but is still necessary. The same goes for the if: the condition is always false, but leaving it out changes the result! If you check for something other than :dynamically_defined_method, the difference also disappears.

One of the few things that (fortunately) doesn't make a difference is using klass.instance_eval or Object.class_eval.

@headius
Copy link
Member

headius commented May 14, 2018

@Confusion Thanks for the extra footwork on this! I am not sure if we can get it done for 9.2, but your reproductions will help us get it fixed soon.

@headius
Copy link
Member

headius commented May 14, 2018

Hmm, at a glance I'd guess that the define_method call is not executing against the proper context, so the defined method goes somewhere else.

@headius
Copy link
Member

headius commented May 14, 2018

Ping @enebo @subbuss for ideas...I have not tried to debug this but something may be obvious to you.

@headius
Copy link
Member

headius commented May 14, 2018

Confirmed broken on master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants