rubinius/rubinius

View on GitHub
core/hash.rb

Summary

Maintainability
F
4 days
Test Coverage
class Hash
  include Enumerable

  Vector = Rubinius::Tuple

  class State
    attr_accessor :head
    attr_accessor :tail
    attr_accessor :size

    def self.from(state)
      new_state = new
      new_state.compare_by_identity if state and state.compare_by_identity?
      new_state
    end

    def initialize
      @size = 0
      @head = nil
      @tail = nil
      @compare_by_identity = false
    end

    private :initialize

    def compare_by_identity?
      @compare_by_identity
    end

    def compare_by_identity
      @compare_by_identity = true
      class << self
        def match?(this_key, this_hash, other_key, other_hash)
          Rubinius::Type.object_equal other_key, this_key
        end
      end
    end

    def match?(this_key, this_hash, other_key, other_hash)
      other_hash == this_hash and
        (Rubinius::Type::object_equal(other_key, this_key) or
          other_key.eql?(this_key))
    end
  end

  class Item
    attr_accessor :previous
    attr_accessor :next
    attr_accessor :key
    attr_accessor :key_hash
    attr_accessor :value

    def initialize(state, key, key_hash, value)
      if key.kind_of?(String) and !key.frozen? and !state.compare_by_identity?
        key = key.dup
        key.freeze
      end

      @key = key
      @key_hash = key_hash
      @value = value

      if tail = state.tail
        @previous = tail
        state.tail = tail.next = self
      else
        state.head = state.tail = self
      end

      state.size += 1
    end

    private :initialize

    def empty?
      not @key_hash
    end

    def lookup(state, key, key_hash)
      return self if state.match? @key, @key_hash, key, key_hash
    end

    def insert(state, key, key_hash, value)
      return unless state.match? @key, @key_hash, key, key_hash
      @value = value
      self
    end

    def delete(state, key, key_hash)
      if state.match? @key, @key_hash, key, key_hash

        if @previous
          @previous.next = @next
        else
          state.head = @next
        end

        if @next
          @next.previous = @previous
        else
          state.tail = @previous
        end

        state.size -= 1
        @key_hash = nil

        self
      end
    end

    def inspect
      "\#<#{self.class}:0x#{object_id.to_s(16)} @key=#{@key} @key_hash=#{@key_hash} @value=#{@value} @previous=\#<#{@previous.class}:0x#{@previous.object_id.to_s(16)}> @next=\#<#{@next.class}:0x#{@next.object_id.to_s(16)}>>"
    end
  end

  class List
    attr_accessor :key_hash
    attr_accessor :entries

    def initialize(key_hash)
      @key_hash = key_hash
      @entries = Vector.new 2
    end

    private :initialize

    def empty?
      not @entries
    end

    def lookup(state, key, key_hash)
      @entries.each { |e| return e if e.lookup(state, key, key_hash) }
      nil
    end

    def insert(state, key, key_hash, value)
      @entries.each { |e| return e if e.insert(state, key, key_hash, value) }

      item = Item.new state, key, key_hash, value
      @entries = @entries.insert_at_index @entries.size, item

      item
    end

    def add(state, item, key, key_hash, value)
      @entries[0] = item
      @entries[1] = Item.new state, key, key_hash, value
    end

    def delete(state, key, key_hash)
      @entries.each_with_index do |e, i|
        if e.delete state, key, key_hash
          @entries = @entries.delete_at_index i
          return e
        end
      end
      nil
    end
  end

  # An Array Mapped Trie
  class Trie
    MAX_LEVELS = 10

    attr_accessor :bmp
    attr_accessor :entries
    attr_accessor :level

    def self.from_key(state, key, key_hash, value)
      trie = new 0
      trie.add_key state, key, key_hash, value
      trie
    end

    def self.from_item(state, level, item, key, key_hash, value)
      if level < MAX_LEVELS
        trie = new level + 1
        trie.add_item state, item, key, key_hash, value
        trie
      else
        list = List.new key_hash
        list.add state, item, key, key_hash, value
        list
      end
    end

    def self.from_none
      trie = new 0
      trie.entries = Vector[]
      trie
    end

    def initialize(level)
      @level = level
      @bmp = 0
      @entries = nil
    end

    private :initialize

    def empty?
      not @entries
    end

    def lookup(state, key, key_hash)
      return unless index = item_index(key_hash)
      @entries[index].lookup state, key, key_hash
    end

    def insert(state, key, key_hash, value)
      if index = item_index(key_hash)
        item = @entries[index]
        if item.insert state, key, key_hash, value
          return item
        else
          obj = Trie.from_item state, @level, item, key, key_hash, value
          @entries[index] = obj
          return obj
        end
      else
        @bmp = set_bitmap key_hash
        index = item_index key_hash
        item = Item.new state, key, key_hash, value
        @entries = @entries.insert_at_index index, item
        return item
      end
    end

    def add_key(state, key, key_hash, value)
      @bmp = set_bitmap key_hash
      item = Item.new state, key, key_hash, value
      @entries = Vector[item]
    end

    def add_item(state, item, key, key_hash, value)
      item_hash = item.key_hash
      bmp = @bmp = set_bitmap(item_hash)
      @bmp = set_bitmap key_hash

      if bmp == @bmp
        if item_hash == key_hash
          list = List.new key_hash
          list.add state, item, key, key_hash, value
          @entries = Vector[list]
        else
          obj = Trie.from_item state, @level, item, key, key_hash, value
          @entries = Vector[obj]
        end
      else
        @entries = Vector.new 2
        @entries[item_index(item_hash)] = item
        @entries[item_index(key_hash)] = Item.new state, key, key_hash, value
      end
    end

    def delete(state, key, key_hash)
      return unless index = item_index(key_hash)

      item = @entries[index]
      if de = item.delete(state, key, key_hash)
        if item.empty?
          @bmp = unset_bitmap key_hash

          @entries = @entries.delete_at_index index
        end

        return de
      end
    end

    def inspect
      s = "\#<#{self.class}:0x#{object_id.to_s(16)} @bmp=#{@bmp} @level=#{@level} @entries="
      if @entries.size > 0
        s << "[ "
        s << @entries.map { |e| "\#<#{e.class}:0x#{e.object_id.to_s(16)}>" }.join(", ")
        s << " ]"
      end
      s << ">"
    end

    def item_index(key_hash)
      Rubinius.invoke_primitive :vm_hash_trie_item_index, key_hash, @level, @bmp
    end

    def set_bitmap(key_hash)
      Rubinius.invoke_primitive :vm_hash_trie_set_bitmap, key_hash, @level, @bmp
    end

    def unset_bitmap(key_hash)
      Rubinius.invoke_primitive :vm_hash_trie_unset_bitmap, key_hash, @level, @bmp
    end
  end

  # An external iterator that returns entries in insertion order.  While
  # somewhat following the API of Enumerator, it is named Iterator because it
  # does not provide <code>#each</code> and should not conflict with
  # +Enumerator+ in MRI 1.8.7+. Returned by <code>Hash#to_iter</code>.

  class Iterator
    def initialize(state)
      @state = state
    end

    private :initialize

    # Returns the next object or +nil+.
    def next(item)
      if item
        return item if item = item.next
      else
        return @state.head
      end
    end
  end

  # Hash methods

  def self.[](*args)
    total = args.size
    if total == 1
      obj = args.first
      if hash = Rubinius::Type.check_convert_type(obj, Hash, :to_hash)
        new_hash = allocate.replace(hash)
        new_hash.default = nil
        return new_hash
      elsif array = Rubinius::Type.check_convert_type(obj, Array, :to_ary)
        hash = allocate
        array.each do |a|
          a = Rubinius::Type.check_convert_type a, Array, :to_ary
          next unless a

          size = a.size
          unless size >= 1 and size <= 2
            raise ArgumentError, "invalid number of elements (#{size} for 1..2)"
          end
          hash[a[0]] = a[1]
        end

        return hash
      end
    end

    return allocate if total == 0

    if total & 1 == 1
      raise ArgumentError, "Expected an even number, got #{total}"
    end

    hash = allocate
    i = 0

    while i < total
      hash[args[i]] = args[i+1]
      i += 2
    end

    hash
  end

  def self.try_convert(obj)
    Rubinius::Type.try_convert obj, Hash, :to_hash
  end

  def self.new_from_literal(size)
    allocate
  end

  def initialize(default=undefined, &block)
    Rubinius.check_frozen

    if !undefined.equal?(default) and block
      raise ArgumentError, "Specify a default value or a block, not both"
    end

    if block
      @default = nil
      @default_proc = block
    elsif !undefined.equal?(default)
      @default = default
      @default_proc = nil
    end

    self
  end
  private :initialize

  def hash
    val = size
    Thread.detect_outermost_recursion self do
      each_item do |item|
        Rubinius.privately { val ^= item.key.hash }
        Rubinius.privately { val ^= item.value.hash }
      end
    end

    return val
  end

  def initialize_copy(other)
    replace other
  end
  private :initialize_copy

  def [](key)
    if @trie
      Rubinius.privately { key_hash = key.hash }
      if item = @trie.lookup(@state, key, key_hash)
        return item.value
      end
    end

    default key
  end

  def []=(key, value)
    Rubinius.check_frozen

    Rubinius.privately { key_hash = key.hash }

    if @trie
      @trie.insert @state, key, key_hash, value
    else
      @state = State.new unless @state
      @trie = Trie.from_key @state, key, key_hash, value
    end

    value
  end

  alias_method :store, :[]=

  def <(other)
    other = Rubinius::Type.coerce_to(other, Hash, :to_hash)
    other > self
  end

  def <=(other)
    other = Rubinius::Type.coerce_to(other, Hash, :to_hash)
    other >= self
  end

  def >(other)
    other = Rubinius::Type.coerce_to(other, Hash, :to_hash)

    return false if size <= other.size

    self >= other
  end

  def >=(other)
    other = Rubinius::Type.coerce_to(other, Hash, :to_hash)

    return false if size < other.size

    other.each do |other_key, other_val|
      val = fetch(other_key, undefined)
      return false if undefined.equal?(val) || val != other_val
    end

    true
  end

  def assoc(key)
    each_item { |e| return e.key, e.value if key == e.key }
    nil
  end

  def clear
    @state = State.from @state
    @trie = nil
    self
  end

  def compare_by_identity
    Rubinius.check_frozen

    @state = State.new unless @state
    @state.compare_by_identity
    self
  end

  def compare_by_identity?
    return false unless @state
    @state.compare_by_identity?
  end

  def default(key=undefined)
    if @default_proc and !undefined.equal?(key)
      @default_proc.call(self, key)
    else
      @default
    end
  end

  def default=(value)
    @default_proc = nil
    @default = value
  end

  def default_proc
    @default_proc
  end

  def default_proc=(prc)
    Rubinius.check_frozen
    unless prc.nil?
      prc = Rubinius::Type.coerce_to prc, Proc, :to_proc

      if prc.lambda? and prc.arity != 2
        raise TypeError, "default proc must have arity 2"
      end
    end

    @default = nil
    @default_proc = prc
  end

  def delete(key)
    Rubinius.check_frozen

    if @trie
      Rubinius.privately { key_hash = key.hash }
      if item = @trie.delete(@state, key, key_hash)
        @trie = nil if @trie.empty?
        return item.value
      end
    end

    return yield(key) if block_given?

    nil
  end

  def delete_if
    return to_enum(:delete_if) { size } unless block_given?

    Rubinius.check_frozen

    each_item { |e| delete e.key if yield(e.key, e.value) }

    self
  end

  def dig(key, *remaining_keys)
    item = self[key]
    return item if remaining_keys.empty? || item.nil?

    raise TypeError, "#{item.class} does not have #dig method" unless item.respond_to?(:dig)

    item.dig(*remaining_keys)
  end

  def each
    return to_enum(:each) { size } unless block_given?

    if @state
      item = @state.head
      while item
        yield [item.key, item.value]
        item = item.next
      end
    end

    self
  end

  alias_method :each_pair, :each

  def each_item
    return unless @state

    item = @state.head
    while item
      yield item
      item = item.next
    end
  end

  def each_key
    return to_enum(:each_key) { size } unless block_given?

    each_item { |e| yield e.key }

    self
  end

  def each_value
    return to_enum(:each_value) { size } unless block_given?

    each_item { |e| yield e.value }

    self
  end

  def empty?
    size == 0
  end

  def eql?(other)
    return true if self.equal? other

    unless other.kind_of? Hash
      return false unless other.respond_to? :to_hash
      return other.eql?(self)
    end

    return false unless other.size == size

    Thread.detect_recursion self, other do
      other.each_item do |e|

        Rubinius.privately { key_hash = e.key.hash }
        return false unless item = @trie.lookup(@state, e.key, key_hash)

        # Order of the comparison matters! We must compare our value with
        # the other Hash's value and not the other way around.
        return false unless item.value.eql?(e.value)
      end
    end

    true
  end

  def ==(other)
    return true if self.equal? other

    unless other.kind_of? Hash
      return false unless other.respond_to? :to_hash
      return other == self
    end

    return false unless other.size == size

    Thread.detect_recursion self, other do
      other.each_item do |e|

        Rubinius.privately { key_hash = e.key.hash }
        item = @trie.lookup(@state, e.key, key_hash)

        return false unless item

        # Order of the comparison matters! We must compare our value with
        # the other Hash's value and not the other way around.
        unless Rubinius::Type::object_equal(item.value, e.value) or
               item.value == e.value
          return false
        end
      end
    end

    true
  end

  def fetch(key, default=undefined)
    if @trie
      Rubinius.privately { key_hash = key.hash }
      if item = @trie.lookup(@state, key, key_hash)
        return item.value
      end
    end

    return yield(key) if block_given?
    return default unless undefined.equal?(default)
    raise KeyError, "key #{key} not found"
  end

  def fetch_values(*keys, &block)
    keys.map { |key| fetch(key, &block) }
  end

  # Searches for an item matching +key+. Returns the item
  # if found. Otherwise returns +nil+. Called from the C-API.
  def find_item(key)
    if @trie
      Rubinius.privately { key_hash = key.hash }
      if item = @trie.lookup(@state, key, key_hash)
        return item
      end
    end

    nil
  end

  def flatten(level=1)
    to_a.flatten(level)
  end

  def initialize_copy(other)
    replace other
  end
  private :initialize_copy

  def inspect
    out = []
    return '{...}' if Thread.detect_recursion self do
      each_item do |e|
        str =  Rubinius::Type.coerce_inspect(e.key)
        str << '=>'
        str << Rubinius::Type.coerce_inspect(e.value)
        out << str
      end
    end

    ret = "{#{out.join ', '}}"
    Rubinius::Type.infect(ret, self) unless empty?
    ret
  end

  alias_method :to_s, :inspect

  def invert
    h = Hash.allocate
    each_item { |e| h[e.value] = e.key }
    h
  end

  def keep_if
    return to_enum(:keep_if) { size } unless block_given?

    Rubinius.check_frozen

    each_item { |e| delete e.key unless yield(e.key, e.value) }

    self
  end

  def key(value)
    each_item { |e| return e.key if e.value == value }

    nil
  end

  alias_method :index, :key

  def key?(key)
    if @trie
      Rubinius.privately { key_hash = key.hash }
      if @trie.lookup(@state, key, key_hash)
        return true
      end
    end

    false
  end

  alias_method :has_key?, :key?
  alias_method :include?, :key?
  alias_method :member?, :key?

  def keys
    return [] unless @state

    ary = Array.new @state.size

    i = 0
    item = @state.head

    while item
      ary[i] = item.key
      i += 1
      item = item.next
    end

    ary
  end

  def merge(other, &block)
    dup.merge!(other, &block)
  end

  def merge!(other)
    Rubinius.check_frozen

    other = Rubinius::Type.coerce_to other, Hash, :to_hash

    return self if other.empty?

    unless @trie
      @state = State.new unless @state
      @trie = Trie.from_none
    end

    if block_given?
      other.each_item do |e|
        key = e.key
        Rubinius.privately { key_hash = key.hash }

        if item = @trie.lookup(@state, key, key_hash)
          item.value = yield(key, item.value, e.value)
        else
          @trie.insert @state, key, key_hash, e.value
        end
      end
    else
      other.each_item do |e|
        key = e.key
        Rubinius.privately { key_hash = key.hash }
        @trie.insert @state, key, key_hash, e.value
      end
    end

    self
  end

  alias_method :update, :merge!

  def rassoc(value)
    each_item { |e| return e.key, e.value if value == e.value }
    nil
  end

  def rehash
    Rubinius.check_frozen

    return if empty?

    item = @state.head
    @state = State.from @state
    @trie = Trie.from_none

    while item
      Rubinius.privately { key_hash = item.key.hash }
      @trie.insert @state, item.key, key_hash, item.value
      item = item.next
    end

    self
  end

  def reject(&block)
    return to_enum(:reject) { size } unless block_given?

    hsh = dup.delete_if(&block)
    hsh.taint if tainted?
    hsh
  end

  def reject!(&block)
    return to_enum(:reject!) { size } unless block_given?

    Rubinius.check_frozen

    unless empty?
      size = @state.size
      delete_if(&block)
      return self if size != @state.size
    end

    nil
  end

  def replace(other)
    Rubinius.check_frozen

    unless other.kind_of? Hash
      other = Rubinius::Type.coerce_to other, Hash, :to_hash
    end
    return self if self.equal? other

    @state = State.from @state
    @state.compare_by_identity if other.compare_by_identity?
    @trie = Trie.from_none

    other.each_item do |e|
      Rubinius.privately { key_hash = e.key.hash }
      @trie.insert @state, e.key, key_hash, e.value
    end

    @default = other.default
    @default_proc = other.default_proc

    self
  end

  def select
    return to_enum(:select) { size } unless block_given?

    hsh = Hash.allocate

    each_item do |e|
      key = e.key
      value = e.value
      hsh[key] = value if yield(key, value)
    end

    hsh
  end

  def select!
    return to_enum(:select!) { size } unless block_given?

    Rubinius.check_frozen

    return if empty?

    size = @state.size
    each_item { |e| delete e.key unless yield(e.key, e.value) }
    return if size == @state.size

    self
  end

  def shift
    Rubinius.check_frozen

    return default(nil) if empty?

    item = @state.head
    Rubinius.privately { key_hash = item.key.hash }
    @trie.delete @state, item.key, key_hash
    @trie = nil if @trie.empty?

    return item.key, item.value
  end

  def size
    @state ? @state.size : 0
  end

  alias_method :length, :size

  def to_a
    if @state
      ary = Array.new size

      i = 0
      item = @state.head

      while item
        ary[i] = [item.key, item.value]
        i += 1
        item = item.next
      end
    else
      ary = []
    end

    Rubinius::Type.infect ary, self
    ary
  end

  # Returns an external iterator for the bins. See +Iterator+
  def to_iter
    Iterator.new @state
  end

  def to_h
    if instance_of? Hash
      self
    else
      Hash.allocate.replace(to_hash)
    end
  end

  def to_hash
    self
  end

  def to_proc
    method(:[]).to_proc
  end

  def value?(value)
    each_item { |e| return true if value == e.value }
    false
  end

  alias_method :has_value?, :value?

  def values
    return [] unless @state

    ary = Array.new size

    i = 0
    item = @state.head

    while item
      ary[i] = item.value
      i += 1
      item = item.next
    end

    ary
  end

  def values_at(*args)
    if empty?
      args.map { |key| default key }
    else
      args.map do |key|
        Rubinius.privately { key_hash = key.hash }
        if item = @trie.lookup(@state, key, key_hash)
          item.value
        else
          default key
        end
      end
    end
  end

  # Used internally in Rubinius to get around subclasses redefining #[]=
  alias_method :__store__, :[]=
end