Fuzion Logo
fuzion-lang.dev — The Fuzion Language Portal

container/Mutable_Hash_Map.fz

# This file is part of the Fuzion language implementation.
#
# The Fuzion language implementation is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, version 3 of the License.
#
# The Fuzion language implementation is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU General Public License along with The
# Fuzion language implementation.  If not, see <https://www.gnu.org/licenses/>.
# -----------------------------------------------------------------------
#
#  Tokiwa Software GmbH, Germany
#
#  Source code of Fuzion standard library feature Mutable_Hash_Map
#
# -----------------------------------------------------------------------
# Mutable_Hash_Map -- a mutable hash map from keys HK to values V
#
# Uses a mutable array internally, when upper_load_factor is reached size is multiplied
# by resize_factor (doubled, tripled, etc.) and elements are copied to new array of this size.
# If lower_load_factor is reached size is divided by resize_factor.
# Insert and retrieve operations on this map are therefore in amortized O(1).
#
module:public Mutable_Hash_Map(
        # the mutate effect
        #
        public LM type : mutate,
        # type of the keys
        #
        public HK type : property.hashable,
        # type of the values
        #
        public V  type,
        # initial size, also the size of the hash map will not decrease below this
        #
        private min_size i32,
        # NYI: use fractions instead of floats (3x)
        # lower boundary at which the size of the internal array is decreased
        #
        private lower_load_factor f32,
        # upper boundary at which the size of the internal array is increased
        #
        private upper_load_factor f32,
        # factor by which the allocated_size is increased if load reaches upper_load_factor
        #
        resize_factor f32 ) ref : Mutable_Map HK V
pre
  debug: min_size > 0
  debug: 0.0 < lower_load_factor < upper_load_factor
  debug: lower_load_factor < upper_load_factor < 1.0
  debug: resize_factor >= 2.0
is
  # number of entries currently in this map
  #
  load := mut 0
  # maximum number of pairs before size is increased
  #
  max_elem => (allocated_size.get.as_f32 * upper_load_factor).ceil.as_i32
  # minimum number of pairs before size is decreased
  #
  min_elem => (allocated_size.get.as_f32 * lower_load_factor).floor.as_i32
  # size of allocated contents array, allows for some empty slots
  #
  allocated_size := mut (min_size.as_f32 / upper_load_factor).ceil.as_i32
  # the contents
  #
  contents := mut (LM.env.new_array (option (tuple HK V)) allocated_size.get.as_i64 nil)
  # calculate the index of k within contents array in case of no conflict
  #
  idx (k HK) =>
    h1 := hash k
    h  := ((h1 << 17) ^ (h1 << 3) ^ h1).cast_to_i64 & i64.max
    h % allocated_size.get.as_i64
  # in case of a collision at given position,
  # return the next alternative position to check
  #
  collision (at i64) =>
    (at + 1) % allocated_size.get.as_i64  # NYI: dumb collision function, check literature and improve!
  # increase internal allocation if upper_load_factor is reached
  #
  increase_if_necessary =>
    if size > max_elem
      change_allocated_size (allocated_size.get.as_f32 * resize_factor).ceil.as_i32
  # decrease internal allocation if lower_load_factor is reached
  #
  decrease_if_necessary =>
    if size < min_elem && max_elem > min_size
      change_allocated_size (allocated_size.get.as_f32 / resize_factor).ceil.as_i32
  # change size of internal allocation to new_alloc_size
  #
  change_allocated_size(new_alloc_size i32)
    pre
      debug: size <= (new_alloc_size.as_f32 * upper_load_factor).ceil.as_i32
      debug: max size min_size >= (new_alloc_size.as_f32 * lower_load_factor).floor.as_i32
  =>
    allocated_size <- new_alloc_size
    old_contents := contents.get.as_array
    contents <- LM.env.new_array (option (tuple HK V)) allocated_size.get.as_i64 nil
    for tup in (old_contents.filter o->o??) do
      (ok, ov) := tup.get
      store (idx ok) ok ov false
  # store key value pair at given index
  # updates value for existing key, does conflict resolution
  #
  store (at i64, k HK, v V, update_size bool) =>
    match contents.get[at]
      nil     =>
        contents.get[at] := (k, v) # insert new key value pair
        if update_size then load <- load.get + 1
      t tuple =>
        (ek, _) := t
        if ek = k                 # no conflict, but remapping of k
          contents.get[at] := (k, v)
        else                      # conflict
          store (collision at) k v update_size
  # number of entries in this map
  #
  public redef size i32 =>
    load.get
  # add key-value pair to this map
  # if key already exists, existing value gets updated
  #
  public redef put(k HK, v V) unit =>
    store (idx k) k v true
    increase_if_necessary
  # get the value k is mapped to or nil if it does not exist
  #
  public redef get(k HK) option V =>
    retrieve (at i64) option V =>
      match contents.get[at]
        nil     => nil
        t tuple =>
          (ek, v) := t
          if ek = k
            v
          else
            retrieve (collision at)
    retrieve (idx k)
  # remove key from map, returns the removed value
  #
  public redef remove(k HK) option V =>
    del(del_idx i64) option V =>
      match contents.get[del_idx]
        nil => nil
        t tuple =>
          (ek, v) := t
          if ek = k
            contents.get[del_idx] := nil
            load <- load.get - 1
            fill_hole del_idx (collision del_idx)
            v
          else
            del (collision del_idx)
    # move elements if they should be in the position of the removed element
    # recursively filles new holes
    #
    fill_hole(cur_idx, next_idx i64) =>
      match contents.get[next_idx]
        nil     => # done
        t tuple =>
          (ek, _) := t
          if (
            # not having passed array end: move elements saved at a larger index
            (next_idx >= cur_idx && idx ek <= cur_idx) ||
            # element index is larger then array index (collision moved it beyond end):
            #   - always move to left
            #   - move to right (beyond beginning into the end) if it can normally be saved at that index
            (idx ek > next_idx && (cur_idx < next_idx || idx ek <= cur_idx)) ||
            # having passed array end: move elements saved at a larger index only if they get moved left (i.e. not beyond the beginning into the end)
            (cur_idx < cur_idx && next_idx < cur_idx && idx ek <= cur_idx))
          then
            contents.get[cur_idx] := t
            contents.get[next_idx] := nil
            fill_hole next_idx (collision next_idx)
          else
            fill_hole cur_idx (collision next_idx)
    return_val := del (idx k)
    decrease_if_necessary
    return_val
  # get a list of all key/value pairs in this map
  #
  public redef items Sequence (tuple HK V) =>
    contents.get.as_array
      .filter o->o??
      .map (o -> o.get (panic "filter failed"))
  # create an immutable map from this
  #
  public redef as_map container.Map HK V =>
    pairs := items
    container.hash_map HK V (pairs.map (.0) ).as_array (pairs.map (.1)).as_array
  # create an empty instance of Mutable_Hash_Map
  #
  public fixed redef type.empty container.Mutable_Hash_Map LM HK V => container.Mutable_Hash_Map LM HK V 11 0.25 0.5 2
last changed: 2025-06-17