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?
Just to make it more fun, Ruby 2.0.0 does not have the same problem! :) (But Ruby 1.9.3 even at p392 does.)