--Apple-Mail-14--1046285326
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
	charset=US-ASCII;
	delsp=yes;
	format=flowed

The following attachment, when run, shows the following behavior:

> % ruby -v ./add_event_hook_weirdness.rb
> ruby 1.8.3 (2005-06-17) [powerpc-darwin8.1.0]
> ./add_event_hook_weirdness.rb:620:in `hash': undefined method  
> `hash' for #<MyMod:0x32c0c4> (NoMethodError)
>     from ./add_event_hook_weirdness.rb:620:in `y'
>     from ./add_event_hook_weirdness.rb:626
>     from ./add_event_hook_weirdness.rb:625:in `times'
>     from ./add_event_hook_weirdness.rb:625

MyMod is a module, mixed in to MyClass via include. MyClass has one  
method (y) and it calls the method (x) included from the module. The  
event hook that we define takes the id and the klass, puts them in an  
array, and uses that as a hash key. This, in turn, tries to call hash  
on the instance of MyMod that was created when MyClass included into  
it (via include_class_new?).

We've tried our best to strip everything down to a 1 file repro with  
no dependencies outside of standard ruby. As a result, we had to  
include some of RubyInline, which makes the file bigger. We added  
tags ("HERE") to mark the start of the real code and the line we  
think is the offender.

We added one extra feature to help debugging. If you define an  
environment variable "HASH" it'll print out the process ID and sleep  
instead of just raising NoMethodError.

> % HASH=1 ruby -v ./add_event_hook_weirdness.rb
> ruby 1.8.3 (2005-06-17) [powerpc-darwin8.1.0]
> MyMod#hash invoked from ./add_event_hook_weirdness.rb:620:in `y'
> connect to 10702 now... sleeping
>   C-c C-c./add_event_hook_weirdness.rb:609:in `sleep': Interrupt
>     from ./add_event_hook_weirdness.rb:609:in `hash'
>     from ./add_event_hook_weirdness.rb:620:in `y'
>     from ./add_event_hook_weirdness.rb:626
>     from ./add_event_hook_weirdness.rb:625:in `times'
>     from ./add_event_hook_weirdness.rb:625

We tried very hard to debug this ourselves, but our internals- 
debugging-fu is not strong enough.


--Apple-Mail-14--1046285326
Content-Transfer-Encoding: 7bit
Content-Type: text/x-ruby-script;
	x-unix-mode=0744;
	name="add_event_hook_weirdness.rb"
Content-Disposition: attachment;
	filename=add_event_hook_weirdness.rb

#!/usr/local/bin/ruby -w

# To find the real code under test, search for "HERE" (twice)

require 'singleton'
require "rbconfig"
require "digest/md5"
require 'ftools'
require 'fileutils'

$TESTING = false unless defined? $TESTING

class CompilationError < RuntimeError; end

##
# The Inline module is the top-level module used. It is responsible
# for instantiating the builder for the right language used,
# compilation/linking when needed, and loading the inlined code into
# the current namespace.

module Inline
  VERSION = '-1'

  $stderr.puts "RubyInline v #{VERSION}" if $DEBUG

  protected

  def self.rootdir
    env = ENV['INLINEDIR'] || ENV['HOME']
    unless defined? @@rootdir and env == @@rootdir and test ?d, @@rootdir then
      rootdir = ENV['INLINEDIR'] || ENV['HOME']
      Dir.mkdir rootdir, 0700 unless test ?d, rootdir
      Dir.assert_secure rootdir
      @@rootdir = rootdir
    end

    @@rootdir
  end

  def self.directory
    directory = File.join(rootdir, ".ruby_inline")
    unless defined? @@directory and directory == @@directory and test ?d, @@directory then
      unless File.directory? directory then
	$stderr.puts "NOTE: creating #{directory} for RubyInline" if $DEBUG
	Dir.mkdir directory, 0700
      end
      Dir.assert_secure directory
      @@directory = directory
    end
    @@directory
  end

  # Inline::C is the default builder used and the only one provided by
  # Inline. It can be used as a template to write builders for other
  # languages. It understands type-conversions for the basic types and
  # can be extended as needed.
  
  class C 

    protected unless $TESTING

    MAGIC_ARITY_THRESHOLD = 2
    MAGIC_ARITY = -1

    @@type_map = {
      'char'          => [ 'NUM2CHR',  'CHR2FIX' ],
      'char *'        => [ 'STR2CSTR', 'rb_str_new2' ],
      'double'        => [ 'NUM2DBL',  'rb_float_new' ],
      'int'           => [ 'FIX2INT',  'INT2FIX' ],
      'long'          => [ 'NUM2INT',  'INT2NUM' ],
      'unsigned int'  => [ 'NUM2UINT', 'UINT2NUM' ],
      'unsigned long' => [ 'NUM2UINT', 'UINT2NUM' ],
      'unsigned'      => [ 'NUM2UINT', 'UINT2NUM' ],
      # Can't do these converters because they conflict with the above:
      # ID2SYM(x), SYM2ID(x), NUM2DBL(x), FIX2UINT(x)
    }

    def ruby2c(type)
      raise ArgumentError, "Unknown type #{type}" unless @@type_map.has_key? type
      @@type_map[type].first
    end

    def c2ruby(type)
      raise ArgumentError, "Unknown type #{type}" unless @@type_map.has_key? type
      @@type_map[type].last
    end

    def strip_comments(src)
      # strip c-comments
      src = src.gsub(/\s*(?:(?:\/\*)(?:(?:(?!\*\/)[\s\S])*)(?:\*\/))/, '')
      # strip cpp-comments
      src.gsub!(/\s*(?:\/\*(?:(?!\*\/)[\s\S])*\*\/|\/\/[^\n]*\n)/, '')
      src
    end
    
    def parse_signature(src, raw=false)

      sig = self.strip_comments(src)
      # strip preprocessor directives
      sig.gsub!(/^\s*\#.*(\\\n.*)*/, '')
      # strip {}s
      sig.gsub!(/\{[^\}]*\}/, '{ }')
      # clean and collapse whitespace
      sig.gsub!(/\s+/, ' ')

      types = 'void|VALUE|' + @@type_map.keys.map{|x| Regexp.escape(x)}.join('|')

      if /(#{types})\s*(\w+)\s*\(([^)]*)\)/ =~ sig then
	return_type, function_name, arg_string = $1, $2, $3
	args = []
	arg_string.split(',').each do |arg|

	  # helps normalize into 'char * varname' form
	  arg = arg.gsub(/\s*\*\s*/, ' * ').strip

	  # if /(#{types})\s+(\w+)\s*$/ =~ arg
	  if /(((#{types})\s*\*?)+)\s+(\w+)\s*$/ =~ arg then
	    args.push([$4, $1])
	  elsif arg != "void" then
	    $stderr.puts "WARNING: '#{arg}' not understood"
	  end
	end

	arity = args.size
	arity = -1 if arity > MAGIC_ARITY_THRESHOLD or raw

	return {
	  'return' => return_type,
	    'name' => function_name,
	    'args' => args,
	   'arity' => arity
	}
      end

      raise SyntaxError, "Can't parse signature: #{sig}"
    end # def parse_signature

    def generate(src, options={})

      if not Hash === options then
        options = {:expand_types=>options}
      end

      expand_types = options[:expand_types]
      singleton = options[:singleton]
      result = self.strip_comments(src)

      signature = parse_signature(src, !expand_types)
      function_name = signature['name']
      return_type = signature['return']
      arity = signature['arity']

      if expand_types then
	prefix = "static VALUE #{function_name}("
	if arity == MAGIC_ARITY then
	  prefix += "int argc, VALUE *argv, VALUE self"
	else
	  prefix += "VALUE self"
	  signature['args'].each do |arg, type|
	    prefix += ", VALUE _#{arg}"
	  end
	end
	prefix += ") {\n"
	if arity == MAGIC_ARITY then
	  count = 0
	  signature['args'].each do |arg, type|
	    prefix += "  #{type} #{arg} = #{ruby2c(type)}(argv[#{count}]);\n"
	    count += 1
	  end
	else
	  signature['args'].each do |arg, type|
	    prefix += "  #{type} #{arg} = #{ruby2c(type)}(_#{arg});\n"
	  end
	end
	# replace the function signature (hopefully) with new sig (prefix)
	result.sub!(/[^;\/\"\>]+#{function_name}\s*\([^\{]+\{/, "\n" + prefix)
	result.sub!(/\A\n/, '') # strip off the \n in front in case we added it
	unless return_type == "void" then
	  raise SyntaxError, "Couldn't find return statement for #{function_name}" unless 
	    result =~ /return/ 
	  result.gsub!(/return\s+([^\;\}]+)/) do
	    "return #{c2ruby(return_type)}(#{$1})"
	  end
	else
	  result.sub!(/\s*\}\s*\Z/, "\nreturn Qnil;\n}")
	end
      else
	prefix = "static #{return_type} #{function_name}("
	result.sub!(/[^;\/\"\>]+#{function_name}\s*\(/, prefix)
	result.sub!(/\A\n/, '') # strip off the \n in front in case we added it
      end

      delta = if result =~ /\A(static.*?\{)/m then
                $1.split(/\n/).size
              else
                warn "WARNING: Can't find signature in #{result.inspect}\n" unless $TESTING
                0
              end

      file, line = caller[1].split(/:/)
      result = "# line #{line.to_i + delta} \"#{file}\"\n" + result unless $DEBUG

      @src << result
      @sig[function_name] = [arity,singleton]

      return result if $TESTING
    end # def generate

    def module_name
      unless defined? @module_name then
        module_name = @mod.name.gsub('::','__')
        md5 = Digest::MD5.new
        @sig.keys.sort_by{|x| x.to_s}.each { |m| md5 << m.to_s }
        @module_name = "Inline_#{module_name}_#{md5.to_s[0,4]}"
      end
      @module_name
    end

    def so_name
      unless defined? @so_name then
        @so_name = "#{Inline.directory}/#{module_name}.#{Config::CONFIG["DLEXT"]}"
      end
      @so_name
    end

    attr_reader :rb_file, :mod
    attr_accessor :mod, :src, :sig, :flags, :libs if $TESTING

    public

    def initialize(mod)
      raise ArgumentError, "Class/Module arg is required" unless Module === mod
      # new (but not on some 1.8s) -> inline -> real_caller|eval
      stack = caller
      meth = stack.shift until meth =~ /in .inline/
      real_caller = stack.first
      real_caller = stack[3] if real_caller =~ /\(eval\)/
      @real_caller = real_caller.split(/:/).first
      @rb_file = File.expand_path(@real_caller)

      @mod = mod
      @src = []
      @sig = {}
      @flags = []
      @libs = []
    end

    ##
    # Attempts to load pre-generated code returning true if it succeeds.

    def load_cache
      begin
        file = File.join("inline", File.basename(so_name))
        if require file then
          dir = Inline.directory
          warn "WARNING: #{dir} exists but is not being used" if test ?d, dir
          return true
        end
      rescue LoadError
      end
      return false
    end

    ##
    # Loads the generated code back into ruby

    def load
      require "#{so_name}" or raise LoadError, "require on #{so_name} failed"
    end

    ##
    # Builds the source file, if needed, and attempts to compile it.

    def build
      so_name = self.so_name
      so_exists = File.file? so_name

      unless so_exists and File.mtime(rb_file) < File.mtime(so_name)
	
	src_name = "#{Inline.directory}/#{module_name}.c"
	old_src_name = "#{src_name}.old"
	should_compare = File.write_with_backup(src_name) do |io|
	  io.puts
	  io.puts "#include \"ruby.h\""
	  io.puts
	  io.puts @src.join("\n\n")
	  io.puts
	  io.puts
	  io.puts "#ifdef __cplusplus"
	  io.puts "extern \"C\" {"
	  io.puts "#endif"
	  io.puts "  void Init_#{module_name}() {"
          io.puts "    VALUE c = rb_cObject;"
          # TODO: use rb_class2path
          @mod.name.split("::").each do |n|
            io.puts "    c = rb_const_get_at(c,rb_intern(\"#{n}\"));"
          end
	  @sig.keys.sort.each do |name|
	    arity, singleton = @sig[name]
            if singleton then
              io.print "    rb_define_singleton_method(c, \"#{name}\", "
            else
	      io.print "    rb_define_method(c, \"#{name}\", "
            end
	    io.puts  "(VALUE(*)(ANYARGS))#{name}, #{arity});"
	  end
	  io.puts
	  io.puts "  }"
	  io.puts "#ifdef __cplusplus"
	  io.puts "}"
	  io.puts "#endif"
	  io.puts
	end

	# recompile only if the files are different
	recompile = true
	if so_exists and should_compare and
            File::compare(old_src_name, src_name, $DEBUG) then
	  recompile = false

	  # Updates the timestamps on all the generated/compiled files.
	  # Prevents us from entering this conditional unless the source
	  # file changes again.
          t = Time.now
	  File.utime(t, t, src_name, old_src_name, so_name)
	end

	if recompile then

	  # extracted from mkmf.rb
	  srcdir  = Config::CONFIG["srcdir"]
	  archdir = Config::CONFIG["archdir"]
	  if File.exist? archdir + "/ruby.h" then
	    hdrdir = archdir
	  elsif File.exist? srcdir + "/ruby.h" then
	    hdrdir = srcdir
	  else
	    $stderr.puts "ERROR: Can't find header files for ruby. Exiting..."
	    exit 1
	  end

	  flags = @flags.join(' ')
	  flags += " #{$INLINE_FLAGS}" if defined? $INLINE_FLAGS# DEPRECATE
	  libs  = @libs.join(' ')
	  libs += " #{$INLINE_LIBS}" if defined? $INLINE_LIBS	# DEPRECATE

	  cmd = "#{Config::CONFIG['LDSHARED']} #{flags} #{Config::CONFIG['CFLAGS']} -I #{hdrdir} -o #{so_name} #{src_name} #{libs}"
	  
          case RUBY_PLATFORM
          when /mswin32/ then
	    cmd += " -link /INCREMENTAL:no /EXPORT:Init_#{module_name}"
          when /i386-cygwin/ then
            cmd += ' -L/usr/local/lib -lruby.dll'
          end

          cmd += " 2> /dev/null" if $TESTING
	  
	  $stderr.puts "Building #{so_name} with '#{cmd}'" if $DEBUG
          `#{cmd}`
          if $? != 0 then
            bad_src_name = src_name + ".bad"
            File.rename src_name, bad_src_name
            raise CompilationError, "error executing #{cmd}: #{$?}\nRenamed #{src_name} to #{bad_src_name}"
          end
	  $stderr.puts "Built successfully" if $DEBUG
	end

      else
	$stderr.puts "#{so_name} is up to date" if $DEBUG
      end # unless (file is out of date)
    end # def build
      
    ##
    # Adds compiler options to the compiler command line.  No
    # preprocessing is done, so you must have all your dashes and
    # everything.
    
    def add_compile_flags(*flags)
      @flags.push(*flags)
    end

    ##
    # Adds linker flags to the link command line.  No preprocessing is
    # done, so you must have all your dashes and everything.
    
    def add_link_flags(*flags)
      @libs.push(*flags)
    end

    ##
    # Registers C type-casts +r2c+ and +c2r+ for +type+.
    
    def add_type_converter(type, r2c, c2r)
      $stderr.puts "WARNING: overridding #{type}" if @@type_map.has_key? type
      @@type_map[type] = [r2c, c2r]
    end

    ##
    # Adds an include to the top of the file. Don't forget to use
    # quotes or angle brackets.
    
    def include(header)
      @src << "#include #{header}"
    end

    ##
    # Adds any amount of text/code to the source
    
    def prefix(code)
      @src << code
    end

    ##
    # Adds a C function to the source, including performing automatic
    # type conversion to arguments and the return value. Unknown type
    # conversions can be extended by using +add_type_converter+.
    
    def c src
      self.generate(src,:expand_types=>true)
    end

    ##
    # Same as +c+, but adds a class function.
    
    def c_singleton src
      self.generate(src,:expand_types=>true,:singleton=>true)
    end
    
    ##
    # Adds a raw C function to the source. This version does not
    # perform any type conversion and must conform to the ruby/C
    # coding conventions.
    
    def c_raw src
      self.generate(src)
    end

    ##
    # Same as +c_raw+, but adds a class function.
    
    def c_raw_singleton src
      self.generate(src, :singleton=>true)
    end

  end # class Inline::C
end # module Inline

class Module

  ##
  # options is a hash that allows you to pass extra data to your
  # builder.  The only key that is guaranteed to exist is :testing.

  attr_reader :options

  ##
  # Extends the Module class to have an inline method. The default
  # language/builder used is C, but can be specified with the +lang+
  # parameter.
  
  def inline(lang = :C, options={})
    case options
    when TrueClass, FalseClass then
      options = { :testing => options  }
    when Hash
      options[:testing] ||= false
    else
      raise ArgumentError, "BLAH"
    end

    builder_class = begin
                      Inline.const_get(lang)
                    rescue NameError
                      require "inline/#{lang}"
                      Inline.const_get(lang)
                    end

    @options = options
    builder = builder_class.new self

    yield builder

    unless options[:testing] then
      unless builder.load_cache then
        builder.build
        builder.load
      end
    end
  end
end

class File

  ##
  # Equivalent to +File::open+ with an associated block, but moves
  # any existing file with the same name to the side first.
  
  def self.write_with_backup(path) # returns true if file already existed
    
    # move previous version to the side if it exists
    renamed = false
    if test ?f, path then
      renamed = true
      File.rename path, path + ".old"
    end

    File.open(path, "w") do |io|
      yield(io)
    end

    return renamed
  end
end # class File

class Dir

  ##
  # +assert_secure+ checks to see that +path+ exists and has minimally
  # writable permissions. If not, it prints an error and exits. It
  # only works on +POSIX+ systems. Patches for other systems are
  # welcome.
  
  def self.assert_secure(path)
    mode = File.stat(path).mode
    unless ((mode % 01000) & 0022) == 0 then
      if $TESTING then
	raise SecurityError, "Directory #{path} is insecure"
      else
	$stderr.puts "#{path} is insecure (#{sprintf('%o', mode)}), needs 0700 for perms. Exiting."
	exit 1
      end
    end
  end
end

# HERE is the start of the real code under test

class BadOptimizer

  include Singleton

  @@data = Hash.new(0)

  def self.start_optimizing
    self.instance.add_event_hook
  end

  ############################################################
  # Inlined Methods:

  inline(:C) do |builder|

    builder.add_type_converter("rb_event_t", '', '')
    builder.add_type_converter("NODE *", '', '')
    builder.add_type_converter("ID", '', '')

    builder.include '"ruby.h"'
    builder.include '"node.h"'

    builder.prefix "static VALUE optimizer_klass = Qnil;
static VALUE data = Qnil;"

    builder.c_raw <<-'EOF'
    static void
    prof_event_hook(rb_event_t event, NODE *node,
                    VALUE self, ID mid, VALUE klass) {

      static int optimizing = 0;

      if (NIL_P(optimizer_klass))
        optimizer_klass = rb_path2class("BadOptimizer");
      if (NIL_P(data))
        data = rb_cv_get(optimizer_klass, "@@data");
      if (optimizing) return;
      optimizing++;

      switch (event) {
      case RUBY_EVENT_CALL:
        {
          VALUE signature;
    
          signature = rb_ary_new2(2);
          rb_ary_store(signature, 0, klass);
          rb_ary_store(signature, 1, ID2SYM(mid));

          rb_hash_aref(data, signature); // HERE is the problem!!!
        }
        break;
      }
      optimizing--;
    }
    EOF

    builder.c <<-'EOF'
      void add_event_hook() {
        rb_add_event_hook(prof_event_hook, RUBY_EVENT_CALL);
      }
    EOF
  end
end

BadOptimizer::start_optimizing

$data = Hash.new(0)

module MyMod
  def hash
    $stderr.puts "MyMod#hash invoked from #{Kernel.caller.first}"
    $stderr.puts "connect to #{$$} now... sleeping"
    Kernel.sleep
  end if ENV.has_key? 'HASH'

  def x
    42
  end
end

class MyClass
  include MyMod
  def y
    x
  end
end

c = MyClass.new
1000.times do
  c.y
end

--Apple-Mail-14--1046285326--