plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


plum-std/Map.plum
5f5d99d 1
module std
b7071c9 2
import std/List
b7071c9 3
import std/Option
b7071c9 4
import std/Array
73b5e55 5
import std/Bool
ca5fd6f 6
import std/Number
29313cb 7
import std/Str
ea9b8c1 8
ea9b8c1 9
# Any type usable as a `Map` key needs a `hash`, so keys landing in different
ea9b8c1 10
# buckets (`hash(k) % BUCKET_COUNT`) can be told apart in O(1) without a full
ea9b8c1 11
# linear scan of the whole map — see `Str.hash`/`Int.hash` in `str.plum`/
ea9b8c1 12
# `int.plum` for the two implementations `libs/std` provides today. Declared
ea9b8c1 13
# here (rather than alongside `ToStr`/`Comparable` in `str.plum`) since `Map`
ea9b8c1 14
# is its only real consumer; like `Comparable`/`Readable`/`Writable`
ea9b8c1 15
# elsewhere in `libs/std`, `Str`/`Int` don't formally list it in their own
ea9b8c1 16
# `implements` clause (that would need them to import `map.plum`, an
ea9b8c1 17
# unwanted dependency direction for such foundational types) — this is
ea9b8c1 18
# checked the same "unenforced, trust the method exists" way those already
ea9b8c1 19
# are (see the README's note on `checkTraitConformance`).
ea9b8c1 20
trait Hashable =
ea9b8c1 21
  hash() -> Int
5f5d99d 22
a271f34 23
# A Pair is a grouping of a key with a value.
a271f34 24
#
a271f34 25
# `toStr` (unlike `Option`'s — see `libs/std/option.plum`'s note on
a271f34 26
# `toOptionStr`) IS safe as an ordinary method here: `Pair[K, V]` is only
a271f34 27
# ever used as `List[Pair[K, V]]`'s element type inside `Map[K, V]`, and
bf629a2 28
# `List[T: ToStr]` already requires its own `T` to support `toStr` —
bf629a2 29
# so a `Pair` used as a `List` element always has `K`/`V: ToStr` too,
a271f34 30
# and `List`'s own eagerly-compiled `join`/`toStr` (which call `.toStr()` on
a271f34 31
# every element) need this to exist for every `Map` specialization anyway.
5f2f962 32
enum Pair[K, V](ToStr) =
5f2f962 33
  | Pair(key: K, val: V)
5f5d99d 34
a271f34 35
  fun toStr(self) -> Str =
a271f34 36
    return "{self.key.toStr()}: {self.val.toStr()}"
a271f34 37
ea9b8c1 38
# Number of buckets every `Map` allocates at construction — fixed for the
ea9b8c1 39
# map's whole lifetime, no rehashing/resizing as it grows. A real hash table
ea9b8c1 40
# would grow this as `size` outpaces it (keeping average bucket length, and
ea9b8c1 41
# so average `get`/`set` cost, roughly constant); that's future work; for now
ea9b8c1 42
# a big-enough fixed count keeps small-to-medium maps fast in practice.
ea9b8c1 43
BUCKET_COUNT = 16
ea9b8c1 44
ea9b8c1 45
# A Map is a hash table: `BUCKET_COUNT` buckets, each an association-list
ea9b8c1 46
# chain (`List[Pair[K, V]]`) of every key hashing into it — O(1) average
ea9b8c1 47
# `get`/`set`/`remove` (versus the single, whole-map linear scan a plain
ea9b8c1 48
# association list costs), degrading to O(n) only if many keys collide into
ea9b8c1 49
# the same bucket.
5f2f962 50
enum Map[K: Hashable, V] =
5f2f962 51
  | Map(buckets: Array[List[Pair[K, V]]], size: Int)
ea9b8c1 52
ea9b8c1 53
  # `Map[Str, Int]()` — explicit generics required (nothing in a no-arg call
ea9b8c1 54
  # pins down `K`/`V`), builds `BUCKET_COUNT` empty bucket lists up front via
ea9b8c1 55
  # `Array`'s own `push`-to-grow (see `libs/std/array.plum`'s header comment
ea9b8c1 56
  # on why `Array` itself has no direct "make me length N" constructor).
ea9b8c1 57
  fun init() -> Map[K, V] =
ea9b8c1 58
    b := Array[List[Pair[K, V]]]()
ea9b8c1 59
    i := 0
ea9b8c1 60
    while i < BUCKET_COUNT
ea9b8c1 61
      b = b.push(List[Pair[K, V]]())
ea9b8c1 62
      i = i + 1
ea9b8c1 63
    return Map(buckets: b, size: 0)
ea9b8c1 64
ea9b8c1 65
  # `k`'s bucket index — `hash`'s sign is unconstrained (a plain `%` on a
ea9b8c1 66
  # negative `Int` stays negative in Plum, matching wasm's signed
ea9b8c1 67
  # `i64.rem_s`), so this folds a negative result back into `[0,
ea9b8c1 68
  # BUCKET_COUNT)` rather than indexing `buckets` out of range.
ea9b8c1 69
  fun bucketIndex(self, k: K) -> Int =
ea9b8c1 70
    m := k.hash() % BUCKET_COUNT
ea9b8c1 71
    return m < 0 ? m + BUCKET_COUNT : m
ea9b8c1 72
ea9b8c1 73
  # Gets the value for `k`, or `None` if absent.
58b01fc 74
  fun get(self, k: K) -> Option[V] =
ea9b8c1 75
    current := self.buckets.get(self.bucketIndex(k)).head
a271f34 76
    while current != None
a271f34 77
      match current
3a2119e 78
        Some(Node(Pair(key, val), _, next)) =>
3a2119e 79
          if key == k
3a2119e 80
            return Some(val)
3a2119e 81
          current = next
a271f34 82
        None =>
a271f34 83
          break
a271f34 84
    return None
58b01fc 85
a271f34 86
  fun has(self, k: K) -> Bool =
a271f34 87
    return self.get(k) != None
a271f34 88
a271f34 89
  # Sets `k`'s value to `v` — replacing an existing entry for `k` in place
a271f34 90
  # (mutating the stored `Pair`'s `val` field) rather than appending a
ea9b8c1 91
  # duplicate, so a repeated `set` on the same key doesn't grow its bucket
ea9b8c1 92
  # unboundedly or leave a stale value reachable by `get`.
a271f34 93
  fun set(self, k: K, v: V) -> Unit =
ea9b8c1 94
    bucket := self.buckets.get(self.bucketIndex(k))
ea9b8c1 95
    current := bucket.head
a271f34 96
    while current != None
a271f34 97
      match current
3a2119e 98
        Some(Node(pair, _, next)) =>
3a2119e 99
          if pair.key == k
3a2119e 100
            pair.val = v
a271f34 101
            return
3a2119e 102
          current = next
a271f34 103
        None =>
a271f34 104
          break
ea9b8c1 105
    bucket.add(Pair(key: k, val: v))
ea9b8c1 106
    self.size = self.size + 1
58b01fc 107
a271f34 108
  # Sets `k`'s value to `v` only if `k` isn't already present.
a271f34 109
  fun putIfAbsent(self, k: K, v: V) -> Unit =
a271f34 110
    if !self.has(k)
a271f34 111
      self.set(k, v)
a271f34 112
ea9b8c1 113
  # Removes `k`'s entry, if present.
a271f34 114
  fun remove(self, k: K) -> Unit =
ea9b8c1 115
    bucket := self.buckets.get(self.bucketIndex(k))
ea9b8c1 116
    current := bucket.head
a271f34 117
    while current != None
a271f34 118
      match current
a271f34 119
        Some(node) =>
a271f34 120
          if node.value.key == k
ea9b8c1 121
            bucket.unlink(node)
ea9b8c1 122
            self.size = self.size - 1
a271f34 123
            return
a271f34 124
          current = node.next
a271f34 125
        None =>
a271f34 126
          break
a271f34 127
a271f34 128
  fun length(self) -> Int =
ea9b8c1 129
    return self.size
a271f34 130
a271f34 131
  fun isEmpty(self) -> Bool =
ea9b8c1 132
    return self.size == 0
a271f34 133
a271f34 134
  fun clear(self) -> Unit =
ea9b8c1 135
    i := 0
ea9b8c1 136
    while i < self.buckets.length()
ea9b8c1 137
      self.buckets.get(i).clear()
ea9b8c1 138
      i = i + 1
ea9b8c1 139
    self.size = 0
a271f34 140
ea9b8c1 141
  # Calls `cb` with each key and value in the map. Order is bucket order,
ea9b8c1 142
  # then insertion order within a bucket — NOT overall insertion order (that
ea9b8c1 143
  # would need a separate side list to track, which nothing here needs today).
a271f34 144
  fun each(self, cb: fn(K, V)) -> Unit =
ea9b8c1 145
    i := 0
ea9b8c1 146
    while i < self.buckets.length()
ea9b8c1 147
      current := self.buckets.get(i).head
ea9b8c1 148
      while current != None
ea9b8c1 149
        match current
ea9b8c1 150
          Some(Node(Pair(key, val), _, next)) =>
ea9b8c1 151
            cb(key, val)
ea9b8c1 152
            current = next
ea9b8c1 153
          None =>
ea9b8c1 154
            break
ea9b8c1 155
      i = i + 1
a271f34 156
a271f34 157
  fun keys(self) -> List[K] =
a271f34 158
    result := List(head: None, tail: None, size: 0)
ea9b8c1 159
    self.each(|k, v| result.add(k))
a271f34 160
    return result
a271f34 161
a271f34 162
  fun values(self) -> List[V] =
a271f34 163
    result := List(head: None, tail: None, size: 0)
ea9b8c1 164
    self.each(|k, v| result.add(v))
a271f34 165
    return result
58b01fc 166
accb2b1 167
  fun map(self, cb: fn(K, V) -> Pair[X, Y]) -> Map[X, Y] =
ea9b8c1 168
    b := Array[List[Pair[X, Y]]]()
ea9b8c1 169
    i := 0
ea9b8c1 170
    while i < BUCKET_COUNT
ea9b8c1 171
      b = b.push(List[Pair[X, Y]]())
ea9b8c1 172
      i = i + 1
ea9b8c1 173
    result := Map(buckets: b, size: 0)
ea9b8c1 174
    j := 0
ea9b8c1 175
    while j < self.buckets.length()
ea9b8c1 176
      current := self.buckets.get(j).head
ea9b8c1 177
      while current != None
ea9b8c1 178
        match current
ea9b8c1 179
          Some(Node(Pair(key, val), _, next)) =>
ea9b8c1 180
            pair := cb(key, val)
ea9b8c1 181
            result.set(pair.key, pair.val)
ea9b8c1 182
            current = next
ea9b8c1 183
          None =>
ea9b8c1 184
            break
ea9b8c1 185
      j = j + 1
accb2b1 186
    return result
c4ee6ec 187
c4ee6ec 188
test "get/set/has round-trip and set replaces an existing key in place"
ea9b8c1 189
  m := Map[Str, Int]()
a9a0147 190
  assert m.has("a") == False
c4ee6ec 191
  m.set("a", 1)
a9a0147 192
  assert m.has("a") == True
a9a0147 193
  assert m.get("a").unwrap() == 1
a9a0147 194
  assert m.length() == 1
c4ee6ec 195
  m.set("a", 2)
a9a0147 196
  assert m.get("a").unwrap() == 2
a9a0147 197
  assert m.length() == 1
c4ee6ec 198
c4ee6ec 199
test "putIfAbsent only sets a key that isn't already present"
ea9b8c1 200
  m := Map[Str, Int]()
c4ee6ec 201
  m.putIfAbsent("a", 1)
c4ee6ec 202
  m.putIfAbsent("a", 2)
a9a0147 203
  assert m.get("a").unwrap() == 1
c4ee6ec 204
c4ee6ec 205
test "remove deletes an entry and clear empties the map"
ea9b8c1 206
  m := Map[Str, Int]()
c4ee6ec 207
  m.set("a", 1)
c4ee6ec 208
  m.set("b", 2)
c4ee6ec 209
  m.remove("a")
a9a0147 210
  assert m.has("a") == False
a9a0147 211
  assert m.length() == 1
c4ee6ec 212
  m.clear()
a9a0147 213
  assert m.isEmpty() == True
c4ee6ec 214
ea9b8c1 215
test "keys/values list every entry's key/value"
ea9b8c1 216
  m := Map[Str, Int]()
c4ee6ec 217
  m.set("a", 1)
c4ee6ec 218
  m.set("b", 2)
ea9b8c1 219
  assert m.length() == 2
ea9b8c1 220
  assert m.keys().contains("a")
ea9b8c1 221
  assert m.keys().contains("b")
ea9b8c1 222
  assert m.values().contains(1)
ea9b8c1 223
  assert m.values().contains(2)
c4ee6ec 224
c4ee6ec 225
test "each calls the callback with every key and value"
ea9b8c1 226
  m := Map[Str, Int]()
c4ee6ec 227
  m.set("a", 1)
c4ee6ec 228
  m.set("b", 2)
c4ee6ec 229
  # A closure captures its outer variables BY VALUE at creation time, so
c4ee6ec 230
  # reassigning a captured `Int` inside the callback wouldn't be visible out
c4ee6ec 231
  # here — mutate a captured `List` (a class, reference semantics) instead.
4a2384c 232
  seen := List[Int]()
c4ee6ec 233
  m.each(|k, v| seen.add(v))
ea9b8c1 234
  assert seen.length() == 2
ea9b8c1 235
  assert seen.contains(1)
ea9b8c1 236
  assert seen.contains(2)
accb2b1 237
accb2b1 238
test "map transforms every key/value pair into a new map, changing the value type"
ea9b8c1 239
  m := Map[Str, Int]()
accb2b1 240
  m.set("a", 1)
accb2b1 241
  m.set("b", 2)
accb2b1 242
  stringified := m.map(|k, v| Pair(key: k, val: v.toStr()))
a9a0147 243
  assert stringified.get("a").unwrap() == "1"
a9a0147 244
  assert stringified.get("b").unwrap() == "2"
a9a0147 245
  assert stringified.length() == 2
ea9b8c1 246
ea9b8c1 247
test "many keys landing in the same bucket still resolve correctly (collision chaining)"
ea9b8c1 248
  m := Map[Int, Int]()
ea9b8c1 249
  i := 0
ea9b8c1 250
  while i < 40
ea9b8c1 251
    m.set(i, i * i)
ea9b8c1 252
    i = i + 1
ea9b8c1 253
  assert m.length() == 40
ea9b8c1 254
  j := 0
ea9b8c1 255
  while j < 40
ea9b8c1 256
    assert m.get(j).unwrap() == j * j
ea9b8c1 257
    j = j + 1
ea9b8c1 258
  m.remove(5)
ea9b8c1 259
  assert m.has(5) == False
ea9b8c1 260
  assert m.get(4).unwrap() == 16
ea9b8c1 261
  assert m.get(21).unwrap() == 441
ea9b8c1 262
  assert m.length() == 39