JoshCheek / seeing_is_believing

Displays the results of every line of code in your file
1.31k stars 54 forks source link

Strange error where exception backtrace shows that it came from SiB #132

Open JoshCheek opened 6 years ago

JoshCheek commented 6 years ago

While playing with it, I changed the arg to BoxedInt#- to be named num2, the backtrace says it comes from sib/the_matrix.rb

I don't really understand what's going on here, maybe a bug.

class Sig
  def initialize
    @known = Hash.new { |h, mod| h[mod] = {} }
    @current = nil
  end
  def declared(mod, param_types)
    @current = [mod, param_types]
    self
  end
  def returns(return_type)
    @current << return_type if @current
    self
  end
  def observed(method)
    return unless @current
    mod, param_types, return_type = @current
    @known[mod][method.name] = {receives: param_types, returns: return_type}
    declared_names = param_types.keys.sort
    observed_names = method.parameters.map(&:last).sort
    declared_names == observed_names or raise TypeError, <<~MSG
      Sig doesn't match the method for #{mod}##{method.name}
        declared: #{declared_names.inspect}
        observed: #{observed_names.inspect}
    MSG
    @current = nil
    self
  end

  def check_call(binding, obj, method_name)
    return unless sig = get_sig(obj, method_name)
    sig[:receives].each do |name, type|
      value = binding.local_variable_get name
      next if value.kind_of? type
      raise TypeError, "#{name} was #{value.inspect}, but expected a #{type.inspect}"
    end
  end

  def check_return(obj, method_name, value)
    return unless sig = get_sig(obj, method_name)
    return if value.kind_of? sig[:returns]
    raise TypeError, "#{method_name} returned #{value.inspect}, but expected a #{sig[:returns].inspect}"
  end

  def get_sig(obj, method_name)
    @known.key?(obj.class) && @known[obj.class][method_name]
  end
end

Sig::INSTANCE = Sig.new

class Module
  def sig(**types)
    Sig::INSTANCE.declared(self, types)
  end
  def method_added(name)
    Sig::INSTANCE.observed instance_method(name)
  rescue TypeError
    $!.set_backtrace caller.drop(1)
    raise
  end
end

erroring = false # need this b/c, tp will still call return (w/ return_value set to nil)
TracePoint.trace :call, :return do |tp|
  next erroring = false if erroring
  begin
    case tp.event
    when :call
      Sig::INSTANCE.check_call tp.binding, tp.self, tp.method_id
    when :return
      Sig::INSTANCE.check_return tp.self, tp.method_id, tp.return_value
    end
  rescue TypeError
    $!.set_backtrace caller.drop(1)
    erroring = true
    raise
  end
end

###############################################

class BoxedInt
  sig(val: Integer).returns(Integer)
  def initialize(val)
    @val = val
  end

  sig(num: Integer).returns(BoxedInt)
  def + num
    BoxedInt.new @val + num
  end

  sig(num: Integer).returns(BoxedInt)
  def - num2
    BoxedInt.new @val - num
  end

  sig.returns(String)
  def inspect
    "BoxedInt.new(#{@val.inspect})"
  end
end

n = BoxedInt.new 2
n + 10     # => 
n - 3      # => 
n + 5 - 3  # => 

# !> /Users/josh/.gem/ruby/2.5.0/gems/seeing_is_believing-3.6.0/lib/seeing_is_believing/the_matrix.rb:78:in `define_method': Sig doesn't match the method for BoxedInt#message (TypeError)
# !>   declared: [:num]
# !>   observed: []
# !> \tfrom /Users/josh/.gem/ruby/2.5.0/gems/seeing_is_believing-3.6.0/lib/seeing_is_believing/the_matrix.rb:78:in `block (2 levels) in <top (required)>'
# !> \tfrom /Users/josh/.gem/ruby/2.5.0/gems/seeing_is_believing-3.6.0/lib/seeing_is_believing/the_matrix.rb:78:in `class_eval'
# !> \tfrom /Users/josh/.gem/ruby/2.5.0/gems/seeing_is_believing-3.6.0/lib/seeing_is_believing/the_matrix.rb:78:in `block in <top (required)>'
# !> /var/folders/7g/mbft22555w3_2nqs_h1kbglw0000gn/T/seeing_is_believing_temp_dir20180607-91489-n76wa7/program.rb:94:in `<class:BoxedInt>': Sig doesn't match the method for BoxedInt#- (TypeError)
# !>   declared: [:num]
# !>   observed: [:num2]
# !> \tfrom /var/folders/7g/mbft22555w3_2nqs_h1kbglw0000gn/T/seeing_is_believing_temp_dir20180607-91489-n76wa7/program.rb:82:in `<main>'
JoshCheek commented 6 years ago

Note that this refactoring did not have the issue:

class Sig
  INSTANCE = new

  def declared(mod, param_types)
    @current = [mod, param_types]
    self
  end

  def returns(return_type)
    @current << return_type if @current
    self
  end

  def observed(method)
    return unless @current
    mod, param_types, return_type = @current
    @current = nil
    known[mod][method.name] = {receives: param_types, returns: return_type}
    declared = param_types.keys.sort
    observed = method.parameters.map(&:last).sort
    declared == observed or raise TypeError, <<~MSG
    Sig doesn't match the method for #{mod}##{method.name}
      declared: #{declared.inspect}
      observed: #{observed.inspect}
    MSG
  end

  def check_call(binding, obj, method_name)
    return unless sig = get_sig(obj, method_name)
    sig[:receives].each do |name, type|
      value = binding.local_variable_get name
      next if value.kind_of? type
      raise TypeError, "#{name} was #{value.inspect}, but expected a #{type.inspect}"
    end
  end

  def check_return(obj, method_name, value)
    return unless sig = get_sig(obj, method_name)
    return if value.kind_of? sig[:returns]
    raise TypeError, "#{method_name} returned #{value.inspect}, but expected a #{sig[:returns].inspect}"
  end

  private

  def known
    @known ||= Hash.new { |h, mod| h[mod] = {} }
  end

  def get_sig(obj, method_name)
    known.key?(obj.class) && known[obj.class][method_name]
  end
end

class Module
  def sig(**types)
    Sig::INSTANCE.declared(self, types)
  end

  def method_added(name)
    Sig::INSTANCE.observed instance_method(name)
  rescue TypeError
    $!.set_backtrace caller.drop(1)
    raise
  end
end

erroring = false # need this b/c, tp will still call return (w/ return_value set to nil)
TracePoint.trace :call, :return do |tp|
  if erroring
    erroring = false
    next
  end
  begin
    case tp.event
    when :call
      Sig::INSTANCE.check_call tp.binding, tp.self, tp.method_id
    when :return
      Sig::INSTANCE.check_return tp.self, tp.method_id, tp.return_value
    end
  rescue TypeError
    $!.set_backtrace caller.drop(1)
    erroring = true
    raise
  end
end

###############################################

class BoxedInt
  sig(val: Integer).returns(Integer)
  def initialize(val)
    @val = val
  end

  sig(num: Integer).returns(BoxedInt)
  def + num2 # ~> TypeError: Sig doesn't match the method for BoxedInt#+\n  declared: [:num]\n  observed: [:num2]\n
    BoxedInt.new @val + num
  end

  sig(num: Integer).returns(BoxedInt)
  def - num
    BoxedInt.new @val - num
  end

  sig.returns(String)
  def inspect
    "BoxedInt.new(#{@val.inspect})"
  end
end

n = BoxedInt.new 2
n + 10     # => 
n - 3      # => 
n + 5 - 3  # => 

# ~> TypeError
# ~> Sig doesn't match the method for BoxedInt#+
# ~>   declared: [:num]
# ~>   observed: [:num2]
# ~>
# ~> /var/folders/7g/mbft22555w3_2nqs_h1kbglw0000gn/T/seeing_is_believing_temp_dir20180607-91903-1lu6wgo/program.rb:97:in `<class:BoxedInt>'
# ~> /var/folders/7g/mbft22555w3_2nqs_h1kbglw0000gn/T/seeing_is_believing_temp_dir20180607-91903-1lu6wgo/program.rb:90:in `<main>'