Skip to content

Instantly share code, notes, and snippets.

@myronmarston
Last active December 14, 2015 17:38
Show Gist options
  • Save myronmarston/5123087 to your computer and use it in GitHub Desktop.
Save myronmarston/5123087 to your computer and use it in GitHub Desktop.
The craziest ruby bug I've ever seen. I don't understand it. At all.

I've discovered a crazy bug that's really confusing me. I'm curious to hear if anyone can explain it.

Here's some code in foo.rb:

class Superclass
  unless ENV['NORMAL_METHOD_DEF']
    define_method :regex do
      /^(\d)$/
    end
  else
    def regex
      /^(\d)$/
    end
  end
end

class Subclass < Superclass
  unless ENV['FORCE_NIL_BLOCK']
    def self.override(name)
      define_method(name) { super() }
    end
 else
    def self.override(name)
      define_method(name) { super(&nil) }
    end
 end

  unless ENV['DONT_PASS_BLOCK']
    override(:regex) { }
  else
    override(:regex)
  end
end

puts "Subclass.new.regex returns a regular expression object:"
puts Subclass.new.regex.inspect
puts
puts "String#match(regex) returns a MatchData object:"
puts "8".match(/^(\d)$/).inspect
puts
puts "But somehow, when I combine these, I can get nil:"
puts "8".match(Subclass.new.regex).inspect
puts
puts "Unless I add a tap block that does nothing:"
puts "8".match(Subclass.new.regex.tap { |s| }).inspect

And the results of running it:

➜ ruby --version
ruby 1.9.3p327 (2012-11-10 revision 37606) [x86_64-darwin11.4.0]
➜ ruby foo.rb 
Subclass.new.regex returns a regular expression object:
/^(\d)$/

String#match(regex) returns a MatchData object:
#<MatchData "8" 1:"8">

But somehow, when I combine these, I can get nil:
nil

Unless I add a tap block that does nothing:
#<MatchData "8" 1:"8">

For some odd reason, String#match(regex) is returning nil when it should be returning a MatchData object. Adding a no-op tap block fixes this, but I have no idea why.

It seems to be related to the fact that Superclass#regex was defined using define_method rather than def; note what happens when I run it with NORMAL_METHOD_DEF=1:

➜ NORMAL_METHOD_DEF=1 ruby foo.rb 
Subclass.new.regex returns a regular expression object:
/^(\d)$/

String#match(regex) returns a MatchData object:
#<MatchData "8" 1:"8">

But somehow, when I combine these, I can get nil:
#<MatchData "8" 1:"8">

Unless I add a tap block that does nothing:
#<MatchData "8" 1:"8">

Also, the fact that I'm passing a block to override(:regex) seems to matter, too; if I run it with DONT_PASS_BLOCK=1 to avoid that, it fixes the problem again:

➜ DONT_PASS_BLOCK=1 ruby foo.rb
Subclass.new.regex returns a regular expression object:
/^(\d)$/

String#match(regex) returns a MatchData object:
#<MatchData "8" 1:"8">

But somehow, when I combine these, I can get nil:
#<MatchData "8" 1:"8">

Unless I add a tap block that does nothing:
#<MatchData "8" 1:"8">

It appears that I can work around the bug by explicitly passing no block in the call to super using &nil:

➜ FORCE_NIL_BLOCK=1 ruby foo.rb
Subclass.new.regex returns a regular expression object:
/^(\d)$/

String#match(regex) returns a MatchData object:
#<MatchData "8" 1:"8">

But somehow, when I combine these, I can get nil:
#<MatchData "8" 1:"8">

Unless I add a tap block that does nothing:
#<MatchData "8" 1:"8">

I don't understand this. At all. I'm not passing a block to Subclass#regex.

Can anyone explain this?

@myronmarston
Copy link
Author

I decided to file a bug against the ruby bug tracker for this:

http://bugs.ruby-lang.org/issues/8059

@alindeman
Copy link

Wow

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