1import { assert, describe, it } from "@effect/vitest"
2import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils"
3import { Effect, Equal, Hash, MutableHashMap as HM, Option, pipe } from "effect"
4import { collectGarbage } from "./utils/gc.ts"
5
6class Key implements Equal.Equal {
7 constructor(readonly a: number, readonly b: number) {}
8
9 [Hash.symbol]() {
10 return Hash.hash(`${this.a}-${this.b}`)
11 }
12
13 [Equal.symbol](that: unknown): boolean {
14 return that instanceof Key && this.a === that.a && this.b === that.b
15 }
16}
17
18class Value implements Equal.Equal {
19 constructor(readonly c: number, readonly d: number) {}
20
21 [Hash.symbol]() {
22 return Hash.hash(`${this.c}-${this.d}`)
23 }
24
25 [Equal.symbol](that: unknown): boolean {
26 return that instanceof Value && this.c === that.c && this.d === that.d
27 }
28}
29
30function key(a: number, b: number): Key {
31 return new Key(a, b)
32}
33
34function value(c: number, d: number): Value {
35 return new Value(c, d)
36}
37
38describe("MutableHashMap", () => {
39 it.effect.skipIf(process.versions.bun !== undefined || process.versions.deno !== undefined)(
40 "does not retain replaced equal keys after removal and reinsertion",
41 () =>
42 Effect.gen(function*() {
43 const references: Array<WeakRef<Key>> = []
44 const control = new WeakRef({})
45 const map = HM.make([key(0, 0), "value"])
46 for (let i = 0; i < 10; i++) {
47 const lookupKey = key(0, 0)
48 const current = HM.get(map, lookupKey)
49 HM.remove(map, lookupKey)
50 HM.set(map, lookupKey, Option.getOrThrow(current))
51 references.push(new WeakRef(lookupKey))
52 }
53 const latest = references.pop()!.deref()!
54 strictEqual(Array.from(HM.keys(map))[0], latest)
55 yield* collectGarbage
56 assert.isUndefined(control.deref())
57 for (const reference of references) assert.isUndefined(reference.deref())
58 assertSome(HM.get(map, key(0, 0)), "value")
59 })
60 )
61
62 it("retains its entries after it is used as a lookup key", () => {
63 const entryKey = key(0, 0)
64 const map = HM.make([entryKey, "value"])
65 const equalMap = HM.make([key(0, 0), "value"])
66 const outer = HM.make([equalMap, true])
67
68 assertSome(HM.get(outer, map), true)
69 assertSome(HM.get(map, key(0, 0)), "value")
70 })
71
72 it("isMutableHashMap", () => {
73 assertTrue(HM.isMutableHashMap(HM.make([0, "a"], [1, "b"])))
74 assertFalse(HM.isMutableHashMap(new Map([[0, "a"]])))
75 })
76
77 it("toString", () => {
78 const map = HM.make(
79 [0, "a"],
80 [1, "b"]
81 )
82
83 strictEqual(
84 String(map),
85 `MutableHashMap([[0,"a"],[1,"b"]])`
86 )
87 })
88
89 it("toJSON", () => {
90 const map = HM.make(
91 [0, "a"],
92 [1, "b"]
93 )
94
95 deepStrictEqual(map.toJSON(), { _id: "MutableHashMap", values: [[0, "a"], [1, "b"]] })
96 })
97
98 it("inspect", () => {
99 if (typeof window !== "undefined") {
100 return
101 }
102 // oxlint-disable-next-line @typescript-eslint/no-require-imports
103 const { inspect } = require("node:util")
104
105 const map = HM.make(
106 [0, "a"],
107 [1, "b"]
108 )
109
110 deepStrictEqual(inspect(map), inspect({ _id: "MutableHashMap", values: [[0, "a"], [1, "b"]] }))
111 })
112
113 it("make", () => {
114 const map = HM.make(
115 [key(0, 0), value(0, 0)],
116 [key(1, 1), value(1, 1)]
117 )
118
119 strictEqual(HM.size(map), 2)
120 assertTrue(pipe(map, HM.has(key(0, 0))))
121 assertTrue(pipe(map, HM.has(key(1, 1))))
122 })
123
124 it("fromIterable", () => {
125 const map = HM.fromIterable([
126 [key(0, 0), value(0, 0)],
127 [key(1, 1), value(1, 1)]
128 ])
129
130 strictEqual(HM.size(map), 2)
131 assertTrue(pipe(map, HM.has(key(0, 0))))
132 assertTrue(pipe(map, HM.has(key(1, 1))))
133 })
134
135 it("iterate", () => {
136 class Hello {
137 [Hash.symbol]() {
138 return 0
139 }
140
141 [Equal.symbol](that: unknown) {
142 return this === that
143 }
144 }
145
146 const a = new Hello()
147 const b = new Hello()
148
149 const map = HM.make(
150 [a, 0],
151 [b, 0]
152 )
153
154 strictEqual(Array.from(map).length, 2)
155 })
156
157 it("get returns the latest value for an equal key", () => {
158 const map = pipe(
159 HM.empty<Key, Value>(),
160 HM.set(key(0, 0), value(0, 0)),
161 HM.set(key(0, 0), value(1, 1))
162 )
163
164 const result = pipe(
165 map,
166 HM.get(key(0, 0))
167 )
168
169 assertSome(result, value(1, 1))
170 })
171
172 it("has", () => {
173 const map = HM.make(
174 [key(0, 0), value(0, 0)],
175 [key(0, 0), value(1, 1)],
176 [key(1, 1), value(2, 2)],
177 [key(1, 1), value(3, 3)],
178 [key(0, 0), value(4, 4)]
179 )
180
181 pipe(
182 map,
183 HM.has(key(0, 0)),
184 assertTrue
185 )
186
187 pipe(
188 map,
189 HM.has(key(1, 1)),
190 assertTrue
191 )
192
193 pipe(
194 map,
195 HM.has(key(4, 4)),
196 assertFalse
197 )
198 })
199
200 it("keys", () => {
201 const map = pipe(
202 HM.empty<Key, Value>(),
203 HM.set(key(0, 0), value(0, 0)),
204 HM.set(key(1, 1), value(1, 1))
205 )
206
207 deepStrictEqual(Array.from(HM.keys(map)), [
208 key(0, 0),
209 key(1, 1)
210 ])
211 })
212
213 it("values", () => {
214 const map = pipe(
215 HM.empty<Key, Value>(),
216 HM.set(key(0, 0), value(0, 0)),
217 HM.set(key(1, 1), value(1, 1))
218 )
219
220 pipe(
221 map,
222 HM.modifyAt(
223 key(0, 0),
224 () => Option.some(value(0, 1))
225 )
226 )
227
228 strictEqual(HM.size(map), 2)
229 assertSome(pipe(map, HM.get(key(0, 0))), value(0, 1))
230
231 pipe(
232 map,
233 HM.modifyAt(
234 key(2, 2),
235 Option.match({
236 onNone: () => Option.some(value(2, 2)),
237 onSome: Option.some
238 })
239 )
240 )
241
242 strictEqual(HM.size(map), 3)
243 assertSome(pipe(map, HM.get(key(2, 2))), value(2, 2))
244
245 pipe(
246 map,
247 HM.modifyAt(
248 key(2, 2),
249 () => Option.none()
250 )
251 )
252
253 strictEqual(HM.size(map), 2)
254 })
255
256 it("remove deletes an equal key in place", () => {
257 const map = pipe(
258 HM.empty<Key, Value>(),
259 HM.set(key(0, 0), value(0, 0)),
260 HM.set(key(1, 1), value(1, 1))
261 )
262
263 strictEqual(HM.size(map), 2)
264
265 pipe(
266 map,
267 HM.has(key(1, 1)),
268 assertTrue
269 )
270
271 pipe(
272 map,
273 HM.remove(key(1, 1))
274 )
275
276 strictEqual(HM.size(map), 1)
277
278 pipe(
279 map,
280 HM.has(key(1, 1)),
281 assertFalse
282 )
283 })
284
285 it("set overwrites equal keys without changing size", () => {
286 const map = pipe(
287 HM.empty<Key, Value>(),
288 HM.set(key(0, 0), value(0, 0)),
289 HM.set(key(0, 0), value(1, 1)),
290 HM.set(key(1, 1), value(2, 2)),
291 HM.set(key(1, 1), value(3, 3)),
292 HM.set(key(0, 0), value(4, 4))
293 )
294
295 deepStrictEqual(Array.from(map), [
296 [key(0, 0), value(4, 4)],
297 [key(1, 1), value(3, 3)]
298 ])
299 })
300
301 it("size", () => {
302 const map = pipe(
303 HM.empty<Key, Value>(),
304 HM.set(key(0, 0), value(0, 0)),
305 HM.set(key(0, 0), value(1, 1)),
306 HM.set(key(1, 1), value(2, 2)),
307 HM.set(key(1, 1), value(3, 3)),
308 HM.set(key(0, 0), value(4, 4))
309 )
310
311 strictEqual(HM.size(map), 2)
312 })
313
314 it("modify updates existing keys and ignores missing keys", () => {
315 const map = pipe(
316 HM.empty<Key, Value>(),
317 HM.set(key(0, 0), value(0, 0)),
318 HM.set(key(1, 1), value(1, 1))
319 )
320
321 pipe(
322 map,
323 HM.modify(key(0, 0), (v) => value(v.c + 1, v.d + 1))
324 )
325
326 assertSome(pipe(map, HM.get(key(0, 0))), value(1, 1))
327
328 pipe(
329 map,
330 HM.modify(key(1, 1), (v) => value(v.c + 1, v.d + 1))
331 )
332
333 assertNone(pipe(
334 map,
335 HM.remove(key(0, 0)),
336 HM.get(key(0, 0))
337 ))
338 })
339
340 it("pipe()", () => {
341 deepStrictEqual(HM.empty<string, string>().pipe(HM.set("key", "value")), HM.make(["key", "value"]))
342 })
343})