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?
I've tried a few things to pin this down. Firstly, I simplified the example as far as possible to this:
As well as your tap technique, there are two other key ways to make it work. First is:
.. so WITHOUT the empty block in the call to override.
And also:
So merely adding a "p" into Subclass#override's method definition.
I decided to try using set_trace_func to debug it. Intriguingly, if you enable tracing, the problem goes away!
So this is certainly an odd one and given it has changed in Ruby 2.0, it's almost certainly a bug IMHO.