import { Equal, Hash, HashMap, Option, Result } from "effect" import { FastCheck as fc } from "effect/testing" import { describe, expect, it } from "vitest" describe("HashMap", () => { describe("constructors", () => { it("empty", () => { const map = HashMap.empty() expect(HashMap.isEmpty(map)).toBe(true) expect(HashMap.size(map)).toBe(0) }) it("make", () => { const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) expect(HashMap.size(map)).toBe(3) expect(HashMap.get(map, "a")).toEqual(Option.some(1)) expect(HashMap.get(map, "b")).toEqual(Option.some(2)) expect(HashMap.get(map, "c")).toEqual(Option.some(3)) }) it("fromIterable", () => { const entries = [["a", 1], ["b", 2], ["c", 3]] as const const map = HashMap.fromIterable(entries) expect(HashMap.size(map)).toBe(3) expect(HashMap.get(map, "a")).toEqual(Option.some(1)) expect(HashMap.get(map, "b")).toEqual(Option.some(2)) expect(HashMap.get(map, "c")).toEqual(Option.some(3)) }) }) describe("basic operations", () => { it("get - existing key", () => { const map = HashMap.make(["a", 1], ["b", 2]) expect(HashMap.get(map, "a")).toEqual(Option.some(1)) expect(HashMap.get(map, "b")).toEqual(Option.some(2)) }) it("get - non-existing key", () => { const map = HashMap.make(["a", 1], ["b", 2]) expect(HashMap.get(map, "c")).toEqual(Option.none()) }) it("has - existing key", () => { const map = HashMap.make(["a", 1], ["b", 2]) expect(HashMap.has(map, "a")).toBe(true) expect(HashMap.has(map, "b")).toBe(true) }) it("has - non-existing key", () => { const map = HashMap.make(["a", 1], ["b", 2]) expect(HashMap.has(map, "c")).toBe(false) }) it("set - new key", () => { const map1 = HashMap.make(["a", 1]) const map2 = HashMap.set(map1, "b", 2) expect(HashMap.size(map2)).toBe(2) expect(HashMap.get(map2, "a")).toEqual(Option.some(1)) expect(HashMap.get(map2, "b")).toEqual(Option.some(2)) // Original should be unchanged expect(HashMap.size(map1)).toBe(1) expect(HashMap.has(map1, "b")).toBe(false) }) it("set - existing key", () => { const map1 = HashMap.make(["a", 1], ["b", 2]) const map2 = HashMap.set(map1, "a", 10) expect(HashMap.size(map2)).toBe(2) expect(HashMap.get(map2, "a")).toEqual(Option.some(10)) expect(HashMap.get(map2, "b")).toEqual(Option.some(2)) // Original should be unchanged expect(HashMap.get(map1, "a")).toEqual(Option.some(1)) }) it("remove - existing key", () => { const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3]) const map2 = HashMap.remove(map1, "b") expect(HashMap.size(map2)).toBe(2) expect(HashMap.has(map2, "a")).toBe(true) expect(HashMap.has(map2, "b")).toBe(false) expect(HashMap.has(map2, "c")).toBe(true) // Original should be unchanged expect(HashMap.size(map1)).toBe(3) expect(HashMap.has(map1, "b")).toBe(true) }) it("remove - non-existing key", () => { const map1 = HashMap.make(["a", 1], ["b", 2]) const map2 = HashMap.remove(map1, "c") expect(map1).toBe(map2) // Should return same reference expect(HashMap.size(map2)).toBe(2) }) it("getUnsafe - existing key", () => { const map = HashMap.make(["a", 1], ["b", 2]) expect(HashMap.getUnsafe(map, "a")).toBe(1) expect(HashMap.getUnsafe(map, "b")).toBe(2) }) it("getUnsafe - non-existing key", () => { const map = HashMap.make(["a", 1]) expect(() => HashMap.getUnsafe(map, "b")).toThrow("HashMap.getUnsafe: key not found") }) }) describe("iterators and getters", () => { it("keys", () => { const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) const keys = Array.from(HashMap.keys(map)).sort() expect(keys).toEqual(["a", "b", "c"]) }) it("values", () => { const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) const values = Array.from(HashMap.values(map)).sort() expect(values).toEqual([1, 2, 3]) }) it("toValues", () => { const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) const values = HashMap.toValues(map).sort() expect(values).toEqual([1, 2, 3]) }) it("entries", () => { const map = HashMap.make(["a", 1], ["b", 2]) const entries = Array.from(HashMap.entries(map)).sort(([a], [b]) => a.localeCompare(b)) expect(entries).toEqual([["a", 1], ["b", 2]]) }) it("toEntries", () => { const map = HashMap.make(["a", 1], ["b", 2]) const entries = HashMap.toEntries(map).sort(([a], [b]) => a.localeCompare(b)) expect(entries).toEqual([["a", 1], ["b", 2]]) }) it("Symbol.iterator", () => { const map = HashMap.make(["a", 1], ["b", 2]) const entries = Array.from(map).sort(([a], [b]) => a.localeCompare(b)) expect(entries).toEqual([["a", 1], ["b", 2]]) }) }) describe("bulk operations", () => { it("removeMany", () => { const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4]) const map2 = HashMap.removeMany(map1, ["b", "d"]) expect(HashMap.size(map2)).toBe(2) expect(HashMap.has(map2, "a")).toBe(true) expect(HashMap.has(map2, "b")).toBe(false) expect(HashMap.has(map2, "c")).toBe(true) expect(HashMap.has(map2, "d")).toBe(false) }) it("setMany", () => { const map1 = HashMap.make(["a", 1], ["b", 2]) const newEntries = [["c", 3], ["d", 4], ["a", 10]] as const // "a" should be overwritten const map2 = HashMap.setMany(map1, newEntries) expect(HashMap.size(map2)).toBe(4) expect(HashMap.get(map2, "a")).toEqual(Option.some(10)) // overwritten expect(HashMap.get(map2, "b")).toEqual(Option.some(2)) // preserved expect(HashMap.get(map2, "c")).toEqual(Option.some(3)) // new expect(HashMap.get(map2, "d")).toEqual(Option.some(4)) // new }) it("setMany - pipe syntax", () => { const map1 = HashMap.empty() const map2 = HashMap.setMany([["x", 100], ["y", 200]])(map1) expect(HashMap.size(map2)).toBe(2) expect(HashMap.get(map2, "x")).toEqual(Option.some(100)) expect(HashMap.get(map2, "y")).toEqual(Option.some(200)) }) it("setMany - different iterables", () => { const map1 = HashMap.make(["existing", 1]) // Test with Map const jsMap = new Map([["from-map", 2], ["another", 3]]) const map2 = HashMap.setMany(map1, jsMap) expect(HashMap.size(map2)).toBe(3) expect(HashMap.get(map2, "existing")).toEqual(Option.some(1)) expect(HashMap.get(map2, "from-map")).toEqual(Option.some(2)) expect(HashMap.get(map2, "another")).toEqual(Option.some(3)) // Test with Set of tuples const setOfTuples = new Set([["from-set", 4]] as const) const map3 = HashMap.setMany(map2, setOfTuples) expect(HashMap.size(map3)).toBe(4) expect(HashMap.get(map3, "from-set")).toEqual(Option.some(4)) }) it("union", () => { const map1 = HashMap.make(["a", 1], ["b", 2]) const map2 = HashMap.make(["b", 20], ["c", 3]) const map3 = HashMap.union(map1, map2) expect(HashMap.size(map3)).toBe(3) expect(HashMap.get(map3, "a")).toEqual(Option.some(1)) expect(HashMap.get(map3, "b")).toEqual(Option.some(20)) // map2 wins expect(HashMap.get(map3, "c")).toEqual(Option.some(3)) }) }) describe("mapping operations", () => { it("map", () => { const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3]) const map2 = HashMap.map(map1, (value, _key) => value * 2) expect(HashMap.size(map2)).toBe(3) expect(HashMap.get(map2, "a")).toEqual(Option.some(2)) expect(HashMap.get(map2, "b")).toEqual(Option.some(4)) expect(HashMap.get(map2, "c")).toEqual(Option.some(6)) }) it("flatMap", () => { const map1 = HashMap.make(["a", 1], ["b", 2]) const map2 = HashMap.flatMap(map1, (value, key) => HashMap.make([key + "1", value], [key + "2", value * 2])) expect(HashMap.size(map2)).toBe(4) expect(HashMap.get(map2, "a1")).toEqual(Option.some(1)) expect(HashMap.get(map2, "a2")).toEqual(Option.some(2)) expect(HashMap.get(map2, "b1")).toEqual(Option.some(2)) expect(HashMap.get(map2, "b2")).toEqual(Option.some(4)) }) }) describe("filtering operations", () => { it("filter", () => { const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4]) const map2 = HashMap.filter(map1, (value) => value % 2 === 0) expect(HashMap.size(map2)).toBe(2) expect(HashMap.has(map2, "a")).toBe(false) expect(HashMap.has(map2, "b")).toBe(true) expect(HashMap.has(map2, "c")).toBe(false) expect(HashMap.has(map2, "d")).toBe(true) }) it("compact", () => { const map1 = HashMap.make( ["a", Option.some(1)], ["b", Option.none()], ["c", Option.some(3)] ) const map2 = HashMap.compact(map1) expect(HashMap.size(map2)).toBe(2) expect(HashMap.get(map2, "a")).toEqual(Option.some(1)) expect(HashMap.has(map2, "b")).toBe(false) expect(HashMap.get(map2, "c")).toEqual(Option.some(3)) }) it("filterMap", () => { const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4]) const map2 = HashMap.filterMap(map1, (value) => value % 2 === 0 ? Result.succeed(value * 2) : Result.failVoid) expect(HashMap.size(map2)).toBe(2) expect(HashMap.get(map2, "b")).toEqual(Option.some(4)) expect(HashMap.get(map2, "d")).toEqual(Option.some(8)) expect(HashMap.has(map2, "a")).toBe(false) expect(HashMap.has(map2, "c")).toBe(false) }) it("filterMap - key argument", () => { const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4]) const map2 = HashMap.filterMap( map1, (value, key) => key < "c" ? Result.succeed(`${key}:${value}`) : Result.failVoid ) expect(HashMap.size(map2)).toBe(2) expect(HashMap.get(map2, "a")).toEqual(Option.some("a:1")) expect(HashMap.get(map2, "b")).toEqual(Option.some("b:2")) expect(HashMap.has(map2, "c")).toBe(false) expect(HashMap.has(map2, "d")).toBe(false) }) }) describe("search operations", () => { it("findFirst returns an entry matching both key and value", () => { const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) const result = HashMap.findFirst(map, (value) => value > 1) expect(result).toEqual(Option.some(["c", 3])) }) it("findFirst - not found", () => { const map = HashMap.make(["a", 1], ["b", 2]) const result = HashMap.findFirst(map, (value) => value > 5) expect(result).toEqual(Option.none()) }) it("some", () => { const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) expect(HashMap.some(map, (value) => value > 2)).toBe(true) expect(HashMap.some(map, (value) => value > 5)).toBe(false) }) it("every", () => { const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) expect(HashMap.every(map, (value) => value > 0)).toBe(true) expect(HashMap.every(map, (value) => value > 1)).toBe(false) }) it("hasBy", () => { const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) expect(HashMap.hasBy(map, (value, key) => key === "b" && value === 2)).toBe(true) expect(HashMap.hasBy(map, (value, key) => key === "b" && value === 5)).toBe(false) }) }) describe("modification operations", () => { it("modifyAt - existing key", () => { const map1 = HashMap.make(["a", 1], ["b", 2]) const map2 = HashMap.modifyAt( map1, "a", (option) => Option.isSome(option) ? Option.some(option.value * 2) : Option.none() ) expect(HashMap.get(map2, "a")).toEqual(Option.some(2)) }) it("modifyAt - non-existing key", () => { const map1 = HashMap.make(["a", 1]) const map2 = HashMap.modifyAt(map1, "b", (option) => Option.isSome(option) ? option : Option.some(10)) expect(HashMap.get(map2, "b")).toEqual(Option.some(10)) }) it("modifyAt - remove via None", () => { const map1 = HashMap.make(["a", 1], ["b", 2]) const map2 = HashMap.modifyAt(map1, "a", () => Option.none()) expect(HashMap.has(map2, "a")).toBe(false) expect(HashMap.has(map2, "b")).toBe(true) }) it("modify", () => { const map1 = HashMap.make(["a", 1], ["b", 2]) const map2 = HashMap.modify(map1, "a", (value) => value * 3) expect(HashMap.get(map2, "a")).toEqual(Option.some(3)) expect(HashMap.get(map2, "b")).toEqual(Option.some(2)) }) }) describe("reduction operations", () => { it("reduce", () => { const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) const sum = HashMap.reduce(map, 0, (acc, value) => acc + value) expect(sum).toBe(6) }) it("forEach visits each key and value", () => { const map = HashMap.make(["a", 1], ["b", 2]) const collected: Array<[string, number]> = [] HashMap.forEach(map, (value, key) => { collected.push([key, value]) }) expect(collected.sort()).toEqual([["a", 1], ["b", 2]]) }) }) describe("mutation helpers", () => { it("mutate allows in-place modifications", () => { const map1 = HashMap.make(["a", 1]) const map2 = HashMap.mutate(map1, (mutable) => { HashMap.set(mutable, "b", 2) HashMap.set(mutable, "c", 3) }) // Original should be unchanged expect(HashMap.size(map1)).toBe(1) // Mutated map should have all entries expect(HashMap.size(map2)).toBe(3) expect(HashMap.get(map2, "a")).toEqual(Option.some(1)) expect(HashMap.get(map2, "b")).toEqual(Option.some(2)) expect(HashMap.get(map2, "c")).toEqual(Option.some(3)) }) it("beginMutation creates distinct mutable instance", () => { const map1 = HashMap.make(["a", 1]) const mutable = HashMap.beginMutation(map1) // Should be different instances expect(map1).not.toBe(mutable) // Mutations on mutable should not affect original HashMap.set(mutable, "b", 2) expect(HashMap.size(map1)).toBe(1) expect(HashMap.size(mutable)).toBe(2) // endMutation returns same instance const immutable = HashMap.endMutation(mutable) expect(mutable).toBe(immutable) }) it("mutations are isolated from original", () => { const original = HashMap.make(["a", 1], ["b", 2]) const mutated = HashMap.mutate(original, (m) => { HashMap.set(m, "a", 100) HashMap.remove(m, "b") HashMap.set(m, "c", 3) }) // Original unchanged expect(HashMap.get(original, "a")).toEqual(Option.some(1)) expect(HashMap.get(original, "b")).toEqual(Option.some(2)) expect(HashMap.has(original, "c")).toBe(false) // Mutated has changes expect(HashMap.get(mutated, "a")).toEqual(Option.some(100)) expect(HashMap.has(mutated, "b")).toBe(false) expect(HashMap.get(mutated, "c")).toEqual(Option.some(3)) }) }) describe("equality and hashing", () => { it("Equal.equals", () => { const map1 = HashMap.make(["a", 1], ["b", 2]) const map2 = HashMap.make(["b", 2], ["a", 1]) // Different order const map3 = HashMap.make(["a", 1], ["b", 3]) // Different value expect(Equal.equals(map1, map2)).toBe(true) expect(Equal.equals(map1, map3)).toBe(false) }) it("Hash.hash", () => { const map1 = HashMap.make(["a", 1], ["b", 2]) const map2 = HashMap.make(["b", 2], ["a", 1]) // Different order expect(Hash.hash(map1)).toBe(Hash.hash(map2)) }) }) describe("custom hash with Equal objects", () => { class Person implements Equal.Equal { constructor(readonly name: string, readonly age: number) {} [Equal.symbol](that: Equal.Equal): boolean { return that instanceof Person && this.name === that.name && this.age === that.age } [Hash.symbol](): number { return Hash.string(this.name) ^ Hash.number(this.age) } } it("should work with Equal objects as keys", () => { const person1 = new Person("Alice", 25) const person2 = new Person("Alice", 25) // Same data, different instance const person3 = new Person("Bob", 30) const map = HashMap.make([person1, "value1"], [person3, "value3"]) // Should find value using structurally equal key expect(HashMap.get(map, person2)).toEqual(Option.some("value1")) expect(HashMap.has(map, person2)).toBe(true) // Should work with set operation const map2 = HashMap.set(map, person2, "updated") expect(HashMap.get(map2, person1)).toEqual(Option.some("updated")) expect(HashMap.size(map2)).toBe(2) // Should not increase size }) }) describe("regressions", () => { class FixedHashKey implements Equal.Equal { constructor(readonly id: string, readonly hash: number) {} [Equal.symbol](that: Equal.Equal): boolean { return that instanceof FixedHashKey && this.id === that.id } [Hash.symbol](): number { return this.hash } } it("keeps entries addressable when bit 31 is present in indexed leaf insert", () => { const bit31Key = new FixedHashKey("bit31", 31) const bit30Key = new FixedHashKey("bit30", 30) let map = HashMap.empty() map = HashMap.set(map, bit31Key, "session1") map = HashMap.set(map, bit30Key, "session2") expect(HashMap.size(map)).toBe(2) expect(HashMap.get(map, bit31Key)).toEqual(Option.some("session1")) expect(HashMap.get(map, bit30Key)).toEqual(Option.some("session2")) }) it("keeps entries addressable when mergeLeaves sees bit 31", () => { const collisionA = new FixedHashKey("collisionA", 31) const collisionB = new FixedHashKey("collisionB", 31) const bit30Key = new FixedHashKey("bit30", 30) let map = HashMap.empty() map = HashMap.set(map, collisionA, "a") map = HashMap.set(map, collisionB, "b") map = HashMap.set(map, bit30Key, "c") expect(HashMap.size(map)).toBe(3) expect(HashMap.get(map, collisionA)).toEqual(Option.some("a")) expect(HashMap.get(map, collisionB)).toEqual(Option.some("b")) expect(HashMap.get(map, bit30Key)).toEqual(Option.some("c")) }) it("keeps all inserted entries addressable across random bit-31-heavy shapes", () => { const entriesArbitrary = fc.array(fc.integer({ min: 0, max: 31 }), { minLength: 0, maxLength: 80 }).chain( (hashes) => { const allHashes = [31, 31, 30, ...hashes] return fc.uniqueArray(fc.uuid(), { minLength: allHashes.length, maxLength: allHashes.length }).map((ids) => allHashes.map((hash, i) => ({ id: ids[i], hash })) ) } ) fc.assert( fc.property(entriesArbitrary, (entries) => { let map = HashMap.empty() const inserted: Array = [] for (const entry of entries) { const key = new FixedHashKey(entry.id, entry.hash) const value = `value-${entry.id}` map = HashMap.set(map, key, value) inserted.push([key, value]) } expect(HashMap.size(map)).toBe(inserted.length) for (const [key, value] of inserted) { expect(HashMap.has(map, key)).toBe(true) expect(HashMap.get(map, key)).toEqual(Option.some(value)) } }), { numRuns: 200 } ) }) }) describe("type guards", () => { it("isHashMap", () => { const map = HashMap.make(["a", 1]) const notMap = { a: 1 } expect(HashMap.isHashMap(map)).toBe(true) expect(HashMap.isHashMap(notMap)).toBe(false) expect(HashMap.isHashMap(null)).toBe(false) expect(HashMap.isHashMap(undefined)).toBe(false) }) }) describe("stress tests", () => { it("handles many inserts, lookups, and removals", () => { let map = HashMap.empty() // Add many entries for (let i = 0; i < 1000; i++) { map = HashMap.set(map, i, `value${i}`) } expect(HashMap.size(map)).toBe(1000) // Check random entries for (let i = 0; i < 100; i++) { const key = Math.floor(Math.random() * 1000) expect(HashMap.get(map, key)).toEqual(Option.some(`value${key}`)) } // Remove half the entries for (let i = 0; i < 500; i++) { map = HashMap.remove(map, i) } expect(HashMap.size(map)).toBe(500) // Verify removals for (let i = 0; i < 500; i++) { expect(HashMap.has(map, i)).toBe(false) } for (let i = 500; i < 1000; i++) { expect(HashMap.has(map, i)).toBe(true) } }) it("should handle hash collisions", () => { // Create objects with same hash but different equality class CollidingKey implements Equal.Equal { constructor(readonly id: number) {} [Equal.symbol](that: Equal.Equal): boolean { return that instanceof CollidingKey && this.id === that.id } [Hash.symbol](): number { return 42 // Same hash for all instances } } const key1 = new CollidingKey(1) const key2 = new CollidingKey(2) const key3 = new CollidingKey(3) let map = HashMap.empty() map = HashMap.set(map, key1, "value1") map = HashMap.set(map, key2, "value2") map = HashMap.set(map, key3, "value3") expect(HashMap.size(map)).toBe(3) expect(HashMap.get(map, key1)).toEqual(Option.some("value1")) expect(HashMap.get(map, key2)).toEqual(Option.some("value2")) expect(HashMap.get(map, key3)).toEqual(Option.some("value3")) // Remove one map = HashMap.remove(map, key2) expect(HashMap.size(map)).toBe(2) expect(HashMap.has(map, key1)).toBe(true) expect(HashMap.has(map, key2)).toBe(false) expect(HashMap.has(map, key3)).toBe(true) }) }) })