plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
cf9073a
— Peter John
2026-11-01T22:49:06+05:30
perf: Map is an insertion-ordered open-addressing hash table (dense entries + probe index), grows at 75%
- plum-std/Http.plum +6 -15
- plum-std/Json.plum +2 -9
- plum-std/Map.plum +265 -85
- plum-std/Set.plum +3 -9
plum-std/Http.plum
CHANGED
|
@@ -48,21 +48,12 @@ extern fun rawHttpRequest(method: Str, url: Str, headers: Str, body: Str) -> Str
|
|
|
48
48
|
# expects for its own `headers` argument.
|
|
49
49
|
fun encodeHeaders(headers: Map[Str, Str]) -> Str =
|
|
50
50
|
buf := Buffer()
|
|
51
|
-
i := 0
|
|
52
|
-
while i < headers.buckets.length()
|
|
53
|
-
bucket := headers.buckets.get(i)
|
|
54
|
-
j := 0
|
|
55
|
-
|
|
51
|
+
for k, v := range headers
|
|
56
|
-
pair := bucket.data.get(j)
|
|
57
|
-
k := pair.key
|
|
58
|
-
v := pair.val
|
|
59
|
-
|
|
52
|
+
if buf.length() > 0
|
|
60
|
-
|
|
53
|
+
buf.write("\n")
|
|
61
|
-
|
|
54
|
+
buf.write(k)
|
|
62
|
-
|
|
55
|
+
buf.write(": ")
|
|
63
|
-
|
|
56
|
+
buf.write(v)
|
|
64
|
-
j = j + 1
|
|
65
|
-
i = i + 1
|
|
66
57
|
return buf.toStr()
|
|
67
58
|
|
|
68
59
|
# The reverse of `encodeHeaders`, for the response headers `rawHttpRequest`
|
plum-std/Json.plum
CHANGED
|
@@ -340,15 +340,8 @@ record JsonParser(src: Str, pos: Int, text: Str) =
|
|
|
340
340
|
# reference (as `appendJsonPair` does to `buf`) does.
|
|
341
341
|
fun jsonMapToStr(pairs: Map[Str, Json]) -> Str =
|
|
342
342
|
buf := Buffer()
|
|
343
|
-
i := 0
|
|
344
|
-
while i < pairs.buckets.length()
|
|
345
|
-
bucket := pairs.buckets.get(i)
|
|
346
|
-
j := 0
|
|
347
|
-
|
|
343
|
+
for k, v := range pairs
|
|
348
|
-
pair := bucket.data.get(j)
|
|
349
|
-
|
|
344
|
+
appendJsonPair(buf, k, v)
|
|
350
|
-
j = j + 1
|
|
351
|
-
i = i + 1
|
|
352
345
|
return buf.toStr()
|
|
353
346
|
|
|
354
347
|
fun appendJsonPair(buf: Buffer, k: Str, v: Json) -> Unit =
|
plum-std/Map.plum
CHANGED
|
@@ -4,87 +4,153 @@ import std/Array
|
|
|
4
4
|
import std/Int
|
|
5
5
|
import std/Float
|
|
6
6
|
import std/Str
|
|
7
|
+
import std/Buffer
|
|
7
8
|
|
|
8
|
-
# A Pair is a grouping of a key with a value.
|
|
9
|
+
# A Pair is a grouping of a key with a value. `k => v` sugar builds Pairs for
|
|
9
|
-
#
|
|
10
|
-
# `
|
|
10
|
+
# `Map(...)`'s variadic init; a Map itself stores no Pairs.
|
|
11
|
-
# and `Map` stores `List[Pair[K, V]]` buckets.
|
|
12
11
|
record Pair[K, V](key: K, val: V) =
|
|
13
12
|
|
|
14
13
|
fun toStr(self) -> Str =
|
|
15
14
|
return "{self.key.toStr()}: {self.val.toStr()}"
|
|
16
15
|
|
|
17
|
-
# A Map is
|
|
16
|
+
# A Map is an open-addressing hash table with an insertion-ordered dense entry
|
|
17
|
+
# store (the layout of Python's dict):
|
|
18
|
+
#
|
|
19
|
+
# index : Array[Int] power-of-two sized probe table (capacity, starts at 16).
|
|
20
|
+
# 0 = empty, 1 = tombstone (deleted), e + 2 = entry e.
|
|
18
|
-
#
|
|
21
|
+
# hashes : Array[Int] per entry: the mixed hash, compared before `==` so
|
|
22
|
+
# most mismatches never call `==`, and reused on rehash.
|
|
23
|
+
# keys : Array[K] per entry (parallel arrays; `Array[V]` of a struct V is
|
|
24
|
+
# vals : Array[V] stored field by field, so inserting allocates nothing).
|
|
25
|
+
# alive : Array[Bool] per entry: false once deleted.
|
|
26
|
+
# used : Int entries appended so far, dead ones included.
|
|
27
|
+
# size : Int live entries.
|
|
28
|
+
#
|
|
19
|
-
#
|
|
29
|
+
# Entries are appended in insertion order; `index` is probed linearly. A delete
|
|
30
|
+
# writes a tombstone into `index` and clears `alive`. The entry arrays hold
|
|
31
|
+
# capacity * 3/4 slots, so `used == capacity * 3/4` is the 75% load limit counting
|
|
32
|
+
# tombstones (every dead entry owns at most one tombstone): at that point the
|
|
33
|
+
# table is rebuilt, doubling the capacity if at least half the slots are live,
|
|
34
|
+
# or just compacting dead entries away otherwise. A tombstone met while
|
|
35
|
+
# inserting a new key is reused.
|
|
20
|
-
#
|
|
36
|
+
# Iteration order is insertion order (a re-`set` of an existing key keeps its
|
|
21
|
-
#
|
|
37
|
+
# original position; a removed-then-re-added key goes to the end).
|
|
22
|
-
# `K` must have a `hash() -> Int` method (duck-typed
|
|
38
|
+
# `K` must have a `hash() -> Int` method (duck-typed) and `==`.
|
|
23
|
-
record Map[K, V](
|
|
39
|
+
record Map[K, V](index: Array[Int], hashes: Array[Int], keys: Array[K], vals: Array[V], alive: Array[Bool], used: Int, size: Int) =
|
|
24
|
-
|
|
25
|
-
# Number of buckets every `Map` allocates at construction — fixed for the
|
|
26
|
-
# map's whole lifetime, no rehashing/resizing as it grows. A real hash table
|
|
27
|
-
# would grow this as `size` outpaces it (keeping average bucket length, and
|
|
28
|
-
# so average `get`/`set` cost, roughly constant); that's future work; for now
|
|
29
|
-
# a big-enough fixed count keeps small-to-medium maps fast in practice.
|
|
30
|
-
BUCKET_COUNT = 16
|
|
31
40
|
|
|
32
41
|
# `Map[Str, Int]()` builds an empty map; `Map(Pair("a", 1), ...)` infers
|
|
33
42
|
# `K`/`V` from its entries (a later duplicate key wins, as in `set`).
|
|
34
|
-
# Builds `BUCKET_COUNT` empty bucket lists up front via `Array`'s `push`-to-grow.
|
|
35
43
|
fun init(entries: ...Pair[K, V]) -> Map[K, V] =
|
|
36
|
-
|
|
44
|
+
m := Map(index: Array[Int](), hashes: Array[Int](), keys: Array[K](), vals: Array[V](), alive: Array[Bool](), used: 0, size: 0)
|
|
37
|
-
i := 0
|
|
38
|
-
while i < BUCKET_COUNT
|
|
39
|
-
b = b.push(List[Pair[K, V]]())
|
|
40
|
-
i = i + 1
|
|
41
|
-
m
|
|
45
|
+
m.allocate(16)
|
|
42
46
|
for e := range entries
|
|
43
47
|
m.set(e.key, e.val)
|
|
44
48
|
return m
|
|
45
49
|
|
|
50
|
+
# Installs empty storage for `cap` index slots (a power of two).
|
|
51
|
+
fun allocate(self, cap: Int) -> Unit =
|
|
52
|
+
n := cap - cap / 4
|
|
53
|
+
self.index = Array[Int]().resize(cap)
|
|
54
|
+
self.hashes = Array[Int]().resize(n)
|
|
55
|
+
self.keys = Array[K]().resize(n)
|
|
56
|
+
self.vals = Array[V]().resize(n)
|
|
57
|
+
self.alive = Array[Bool]().resize(n)
|
|
58
|
+
self.used = 0
|
|
59
|
+
self.size = 0
|
|
60
|
+
|
|
46
|
-
# `k`'s
|
|
61
|
+
# `k`'s hash, avalanched so identity hashes (`Int`) and sequential keys
|
|
47
|
-
# negative `Int` stays negative in Plum, matching wasm's signed
|
|
48
|
-
#
|
|
62
|
+
# spread across the low bits that pick the slot.
|
|
49
|
-
# BUCKET_COUNT)` rather than indexing `buckets` out of range.
|
|
50
|
-
fun
|
|
63
|
+
fun hashOf(self, k: K) -> Int =
|
|
51
|
-
|
|
64
|
+
h := k.hash()
|
|
52
|
-
|
|
65
|
+
h = {h ^ {h >> 32}} * 0x5851f42d4c957f2d
|
|
66
|
+
return h ^ {h >> 29}
|
|
67
|
+
|
|
68
|
+
# The `index` slot holding `k` (whose hash is `h`), or -1.
|
|
69
|
+
fun findSlot(self, k: K, h: Int) -> Int =
|
|
70
|
+
mask := self.index.length() - 1
|
|
71
|
+
slot := h & mask
|
|
72
|
+
while true
|
|
73
|
+
x := self.index.get(slot)
|
|
74
|
+
if x == 0
|
|
75
|
+
return -1
|
|
76
|
+
if x >= 2
|
|
77
|
+
e := x - 2
|
|
78
|
+
if self.hashes.get(e) == h && self.keys.get(e) == k
|
|
79
|
+
return slot
|
|
80
|
+
slot = {slot + 1} & mask
|
|
81
|
+
return -1
|
|
53
82
|
|
|
54
83
|
# Gets the value for `k`, or fails with `KeyNotFound` if absent.
|
|
55
84
|
fun get(self, k: K) -> error{KeyNotFound}!V =
|
|
56
|
-
|
|
85
|
+
slot := self.findSlot(k, self.hashOf(k))
|
|
57
|
-
i := 0
|
|
58
|
-
while i < bucket.size
|
|
59
|
-
pair := bucket.data.get(i)
|
|
60
|
-
|
|
86
|
+
if slot < 0
|
|
61
|
-
return pair.val
|
|
62
|
-
i = i + 1
|
|
63
|
-
|
|
87
|
+
return error.KeyNotFound
|
|
88
|
+
return self.vals.get(self.index.get(slot) - 2)
|
|
64
89
|
|
|
65
90
|
# Gets the value for `k`, or null if absent.
|
|
66
91
|
fun getOrNull(self, k: K) -> V? =
|
|
67
92
|
return self.get(k) catch return null
|
|
68
93
|
|
|
69
94
|
fun has(self, k: K) -> Bool =
|
|
95
|
+
return self.findSlot(k, self.hashOf(k)) >= 0
|
|
96
|
+
|
|
97
|
+
# Rebuilds the table when the entry arrays are full: doubles the capacity if
|
|
98
|
+
# at least half the entries are live, else compacts at the same capacity.
|
|
99
|
+
fun rebuild(self) -> Unit =
|
|
100
|
+
cap := self.index.length()
|
|
101
|
+
oldHashes := self.hashes
|
|
102
|
+
oldKeys := self.keys
|
|
103
|
+
oldVals := self.vals
|
|
104
|
+
oldAlive := self.alive
|
|
105
|
+
oldUsed := self.used
|
|
106
|
+
n := cap - cap / 4
|
|
107
|
+
newCap := cap
|
|
108
|
+
if self.size * 2 >= n
|
|
109
|
+
newCap = cap * 2
|
|
110
|
+
self.allocate(newCap)
|
|
111
|
+
mask := newCap - 1
|
|
112
|
+
e := 0
|
|
113
|
+
while e < oldUsed
|
|
114
|
+
if oldAlive.get(e)
|
|
115
|
+
h := oldHashes.get(e)
|
|
116
|
+
slot := h & mask
|
|
117
|
+
while self.index.get(slot) != 0
|
|
118
|
+
slot = {slot + 1} & mask
|
|
119
|
+
j := self.used
|
|
120
|
+
self.index.set(slot, j + 2)
|
|
121
|
+
self.hashes.set(j, h)
|
|
122
|
+
self.keys.set(j, oldKeys.get(e))
|
|
123
|
+
self.vals.set(j, oldVals.get(e))
|
|
124
|
+
self.alive.set(j, true)
|
|
70
|
-
|
|
125
|
+
self.used = j + 1
|
|
71
|
-
return false
|
|
72
|
-
|
|
126
|
+
e = e + 1
|
|
73
|
-
|
|
127
|
+
self.size = self.used
|
|
128
|
+
|
|
74
|
-
# Sets `k`'s value to `v`
|
|
129
|
+
# Sets `k`'s value to `v`, replacing an existing entry for `k` in place.
|
|
75
|
-
# (mutating the stored `Pair`'s `val` field) rather than appending a
|
|
76
|
-
# duplicate, so a repeated `set` on the same key doesn't grow its bucket
|
|
77
|
-
# unboundedly or leave a stale value reachable by `get`.
|
|
78
130
|
fun set(self, k: K, v: V) -> Unit =
|
|
79
|
-
bucket := self.buckets.get(self.bucketIndex(k))
|
|
80
|
-
i := 0
|
|
81
|
-
|
|
131
|
+
h := self.hashOf(k)
|
|
82
|
-
|
|
132
|
+
slot := self.findSlot(k, h)
|
|
83
|
-
|
|
133
|
+
if slot >= 0
|
|
84
|
-
|
|
134
|
+
self.vals.set(self.index.get(slot) - 2, v)
|
|
85
|
-
|
|
135
|
+
return
|
|
136
|
+
if self.used >= self.hashes.length()
|
|
137
|
+
self.rebuild()
|
|
138
|
+
mask := self.index.length() - 1
|
|
139
|
+
slot = h & mask
|
|
140
|
+
tomb := -1
|
|
141
|
+
while self.index.get(slot) != 0
|
|
142
|
+
if tomb < 0 && self.index.get(slot) == 1
|
|
143
|
+
tomb = slot
|
|
144
|
+
slot = {slot + 1} & mask
|
|
145
|
+
if tomb >= 0
|
|
146
|
+
slot = tomb
|
|
147
|
+
e := self.used
|
|
148
|
+
self.index.set(slot, e + 2)
|
|
149
|
+
self.hashes.set(e, h)
|
|
150
|
+
self.keys.set(e, k)
|
|
151
|
+
self.vals.set(e, v)
|
|
152
|
+
self.alive.set(e, true)
|
|
86
|
-
|
|
153
|
+
self.used = e + 1
|
|
87
|
-
bucket.push(Pair(key: k, val: v))
|
|
88
154
|
self.size = self.size + 1
|
|
89
155
|
|
|
90
156
|
# Sets `k`'s value to `v` only if `k` isn't already present.
|
|
@@ -92,16 +158,13 @@ record Map[K, V](buckets: Array[List[Pair[K, V]]], size: Int) =
|
|
|
92
158
|
if !self.has(k)
|
|
93
159
|
self.set(k, v)
|
|
94
160
|
|
|
95
|
-
# Removes `k`'s entry, if present.
|
|
161
|
+
# Removes `k`'s entry, if present (leaves a tombstone).
|
|
96
162
|
fun remove(self, k: K) -> Unit =
|
|
97
|
-
|
|
163
|
+
slot := self.findSlot(k, self.hashOf(k))
|
|
98
|
-
|
|
164
|
+
if slot >= 0
|
|
99
|
-
while i < bucket.size
|
|
100
|
-
|
|
165
|
+
self.alive.set(self.index.get(slot) - 2, false)
|
|
101
|
-
|
|
166
|
+
self.index.set(slot, 1)
|
|
102
|
-
|
|
167
|
+
self.size = self.size - 1
|
|
103
|
-
return
|
|
104
|
-
i = i + 1
|
|
105
168
|
|
|
106
169
|
fun length(self) -> Int =
|
|
107
170
|
return self.size
|
|
@@ -110,28 +173,18 @@ record Map[K, V](buckets: Array[List[Pair[K, V]]], size: Int) =
|
|
|
110
173
|
return self.size == 0
|
|
111
174
|
|
|
112
175
|
fun clear(self) -> Unit =
|
|
113
|
-
i := 0
|
|
114
|
-
while i < self.buckets.length()
|
|
115
|
-
self.buckets.get(i).clear()
|
|
116
|
-
i = i + 1
|
|
117
|
-
self.
|
|
176
|
+
self.allocate(16)
|
|
118
177
|
|
|
119
|
-
# `for k, v := range m` (or `m.entries()`): every key with its value, in
|
|
178
|
+
# `for k, v := range m` (or `m.entries()`): every key with its value, in
|
|
120
|
-
#
|
|
179
|
+
# insertion order. One loop, so `break` and `continue` in the body act on the
|
|
121
|
-
#
|
|
180
|
+
# whole walk; the cursor moves before the `yield` for the same reason.
|
|
122
|
-
# `yield` for the same reason.
|
|
123
181
|
fun* entries(self) *(K, V) =
|
|
124
182
|
i := 0
|
|
183
|
+
while i < self.used
|
|
125
|
-
|
|
184
|
+
e := i
|
|
126
|
-
while i < self.buckets.length()
|
|
127
|
-
bucket := self.buckets.get(i)
|
|
128
|
-
if j >= bucket.size
|
|
129
|
-
|
|
185
|
+
i = i + 1
|
|
130
|
-
j = 0
|
|
131
|
-
continue
|
|
132
|
-
|
|
186
|
+
if self.alive.get(e)
|
|
133
|
-
j = j + 1
|
|
134
|
-
|
|
187
|
+
yield self.keys.get(e), self.vals.get(e)
|
|
135
188
|
|
|
136
189
|
# `for k := range m.keys()`.
|
|
137
190
|
fun* keys(self) *K =
|
|
@@ -143,6 +196,8 @@ record Map[K, V](buckets: Array[List[Pair[K, V]]], size: Int) =
|
|
|
143
196
|
for k, v := range self
|
|
144
197
|
yield v
|
|
145
198
|
|
|
199
|
+
struct MapPoint(x: Float, y: Float)
|
|
200
|
+
|
|
146
201
|
test "get/set/has round-trip and set replaces an existing key in place"
|
|
147
202
|
m := Map[Str, Int]()
|
|
148
203
|
assert m.has("a") == false
|
|
@@ -333,3 +388,128 @@ test "init_negative_int_keys_and_duplicates"
|
|
|
333
388
|
assert m.length() == 2
|
|
334
389
|
assert m.get(-5)! == "c"
|
|
335
390
|
assert m.get(-17)! == "b"
|
|
391
|
+
|
|
392
|
+
test "100k inserts, every key found, 100k missing keys absent"
|
|
393
|
+
m := Map[Int, Int]()
|
|
394
|
+
i := 0
|
|
395
|
+
while i < 100000
|
|
396
|
+
m.set(i, i * 2)
|
|
397
|
+
i = i + 1
|
|
398
|
+
assert m.length() == 100000
|
|
399
|
+
i = 0
|
|
400
|
+
bad := 0
|
|
401
|
+
while i < 100000
|
|
402
|
+
if m.get(i)! != i * 2
|
|
403
|
+
bad = bad + 1
|
|
404
|
+
i = i + 1
|
|
405
|
+
assert bad == 0
|
|
406
|
+
i = 100000
|
|
407
|
+
found := 0
|
|
408
|
+
while i < 200000
|
|
409
|
+
if m.has(i)
|
|
410
|
+
found = found + 1
|
|
411
|
+
i = i + 1
|
|
412
|
+
assert found == 0
|
|
413
|
+
|
|
414
|
+
test "delete half, reinsert, tombstones are reused and lookups stay right"
|
|
415
|
+
m := Map[Int, Int]()
|
|
416
|
+
i := 0
|
|
417
|
+
while i < 20000
|
|
418
|
+
m.set(i, i)
|
|
419
|
+
i = i + 1
|
|
420
|
+
i = 0
|
|
421
|
+
while i < 20000
|
|
422
|
+
m.remove(i)
|
|
423
|
+
i = i + 2
|
|
424
|
+
assert m.length() == 10000
|
|
425
|
+
i = 0
|
|
426
|
+
bad := 0
|
|
427
|
+
while i < 20000
|
|
428
|
+
if m.has(i) != {i % 2 == 1}
|
|
429
|
+
bad = bad + 1
|
|
430
|
+
i = i + 1
|
|
431
|
+
assert bad == 0
|
|
432
|
+
i = 0
|
|
433
|
+
while i < 20000
|
|
434
|
+
m.set(i, i + 1)
|
|
435
|
+
i = i + 2
|
|
436
|
+
assert m.length() == 20000
|
|
437
|
+
i = 0
|
|
438
|
+
while i < 20000
|
|
439
|
+
if m.get(i)! != {if i % 2 == 0 then i + 1 else i}
|
|
440
|
+
bad = bad + 1
|
|
441
|
+
i = i + 1
|
|
442
|
+
assert bad == 0
|
|
443
|
+
|
|
444
|
+
test "repeated set/remove of a few keys never grows the table without bound"
|
|
445
|
+
m := Map[Int, Int]()
|
|
446
|
+
i := 0
|
|
447
|
+
while i < 100000
|
|
448
|
+
m.set(i % 7, i)
|
|
449
|
+
m.remove(i % 7)
|
|
450
|
+
i = i + 1
|
|
451
|
+
assert m.length() == 0
|
|
452
|
+
m.set(3, 9)
|
|
453
|
+
assert m.get(3)! == 9
|
|
454
|
+
assert m.index.length() == 16
|
|
455
|
+
|
|
456
|
+
test "growth past several doublings"
|
|
457
|
+
m := Map[Int, Int]()
|
|
458
|
+
assert m.index.length() == 16
|
|
459
|
+
i := 0
|
|
460
|
+
while i < 1000
|
|
461
|
+
m.set(i, i)
|
|
462
|
+
i = i + 1
|
|
463
|
+
assert m.index.length() == 2048
|
|
464
|
+
assert m.length() == 1000
|
|
465
|
+
|
|
466
|
+
test "Map[Str, Int] with Buffer-built keys"
|
|
467
|
+
m := Map[Str, Int]()
|
|
468
|
+
i := 0
|
|
469
|
+
while i < 2000
|
|
470
|
+
b := Buffer()
|
|
471
|
+
b.write("k")
|
|
472
|
+
b.write(i.toStr())
|
|
473
|
+
m.set(b.toStr(), i)
|
|
474
|
+
i = i + 1
|
|
475
|
+
assert m.length() == 2000
|
|
476
|
+
assert m.get("k1999")! == 1999
|
|
477
|
+
assert m.has("k2000") == false
|
|
478
|
+
|
|
479
|
+
test "Map[Int, MapPoint] struct values"
|
|
480
|
+
m := Map[Int, MapPoint]()
|
|
481
|
+
i := 0
|
|
482
|
+
while i < 500
|
|
483
|
+
m.set(i, MapPoint(x: Float(i), y: Float(i) * 2.0))
|
|
484
|
+
i = i + 1
|
|
485
|
+
v := m.get(499)!
|
|
486
|
+
assert v.x == 499.0
|
|
487
|
+
assert v.y == 998.0
|
|
488
|
+
m.set(499, MapPoint(x: 1.0, y: 2.0))
|
|
489
|
+
assert m.get(499)!.y == 2.0
|
|
490
|
+
assert m.length() == 500
|
|
491
|
+
|
|
492
|
+
test "iteration is insertion order, skipping removed entries"
|
|
493
|
+
m := Map[Int, Int]()
|
|
494
|
+
m.set(30, 1)
|
|
495
|
+
m.set(10, 2)
|
|
496
|
+
m.set(20, 3)
|
|
497
|
+
m.set(10, 4)
|
|
498
|
+
m.remove(30)
|
|
499
|
+
m.set(30, 5)
|
|
500
|
+
order := ""
|
|
501
|
+
for k, v := range m
|
|
502
|
+
order = "{order}{k}={v} "
|
|
503
|
+
assert order == "10=4 20=3 30=5 "
|
|
504
|
+
|
|
505
|
+
test "clear then reuse"
|
|
506
|
+
m := Map[Int, Int]()
|
|
507
|
+
i := 0
|
|
508
|
+
while i < 100
|
|
509
|
+
m.set(i, i)
|
|
510
|
+
i = i + 1
|
|
511
|
+
m.clear()
|
|
512
|
+
assert m.length() == 0
|
|
513
|
+
assert m.has(5) == false
|
|
514
|
+
m.set(5, 1)
|
|
515
|
+
assert m.get(5)! == 1
|
plum-std/Set.plum
CHANGED
|
@@ -31,17 +31,11 @@ record Set[T](map: Map[T, Bool]) =
|
|
|
31
31
|
fun clear(self) -> Unit =
|
|
32
32
|
self.map.clear()
|
|
33
33
|
|
|
34
|
-
# Every member, in
|
|
34
|
+
# Every member, in insertion order.
|
|
35
35
|
fun toList(self) -> List[T] =
|
|
36
36
|
result := List[T]()
|
|
37
|
-
i := 0
|
|
38
|
-
while i < self.map.buckets.length()
|
|
39
|
-
|
|
37
|
+
for k := range self.map.keys()
|
|
40
|
-
j := 0
|
|
41
|
-
while j < bucket.size
|
|
42
|
-
|
|
38
|
+
result.push(k)
|
|
43
|
-
j = j + 1
|
|
44
|
-
i = i + 1
|
|
45
39
|
return result
|
|
46
40
|
|
|
47
41
|
test "set_dedup: adding duplicates keeps one copy"
|