diff --git a/swiss_table/README.md b/swiss_table/README.md
new file mode 100644
index 0000000..8af07d4
--- /dev/null
+++ b/swiss_table/README.md
@@ -0,0 +1,117 @@
+# FastSwissMap
+
+
+ Русский ・ English
+
+
+---
+
+## Русский
+
+FastSwissMap — хеш-таблица общего назначения, построенная на двух идеях: swiss table (батчевый поиск по control-байтам внутри группы из 8 слотов) и extendible hashing (директория указателей на bucket'ы, растущая делением, а не полным рехэшем).
+
+Обычная swiss table при переполнении рехэширует всю таблицу целиком. Здесь вместо этого каждый bucket — отдельная небольшая swiss table. Пока bucket не достиг `maxBucketCapacity` (64 слота), он просто удваивается на месте. Когда предел достигнут, bucket расщепляется на два по одному биту хэша, а рехэшу подвергаются только записи этого одного bucket'а, а не вся карта.
+
+### Внутреннее устройство
+
+**Control-байты и группы.** Каждая группа занимает один `uint64` (`groupSize = 8`, по байту на слот). Пустой слот кодируется как `0b10000000`, удалённый (tombstone) — как `0b11111110`. Поиск совпадающего байта в группе делает `matchByte` — трюк SWAR (широкое XOR + вычитание + маска знаковых битов), который находит все позиции сразу, без цикла по восьми байтам. У этого трюка есть цена: вычитание всего 64-битного слова разом может дать ложное совпадение из-за переноса между байтами (borrow chain), поэтому кандидаты из маски перепроверяются побайтово в `firstVerified`.
+
+**Разбиение хэша.** `splitHash` делит 64-битный хэш на `h1` (верхние 57 бит — индекс в директории плюс затравка пробинга внутри bucket'а) и `h2` (младшие 7 бит — то, что кладётся в control-байт). Индекс в директории берётся как верхние `globalDepth` бит хэша (`dirIndex`), то есть один и тот же топ-срез бит определяет и позицию в директории, и по какую сторону разреза окажется запись при расщеплении bucket'а.
+
+**Пробинг.** Внутри bucket'а группы перебираются квадратичным пробингом — смещения растут по треугольным числам (0, 1, 3, 6, 10, ...), а не на единицу за шаг, что снижает первичную кластеризацию. Последовательность биективна по модулю числа групп, поэтому цикл `probe < b.groups` гарантированно обходит каждую группу ровно один раз и завершается.
+
+**growthLeft.** Счётчик реально свободных (`ctrlEmpty`) слотов. Удаление не восстанавливает его — только помечает слот как tombstone или, если группа никогда не была заполнена целиком, сразу как `ctrlEmpty`. Если бы tombstone восполнял growthLeft, bucket мог бы физически забиться удалёнными байтами при малом числе живых записей и никогда не вырасти: `Get`/`Put` крутились бы в поиске пустого слота, которого физически не осталось.
+
+**Удаление.** Если в группе целевого слота уже есть хотя бы один `ctrlEmpty`, значит группа никогда не заполнялась полностью и не участвует в обрыве цепочки пробинга — слот сразу понижается до `ctrlEmpty`, а `growthLeft` растёт. Иначе ставится tombstone: без него цепочка пробинга для других ключей, проходящая через этот слот, оборвалась бы преждевременно.
+
+**Рост bucket'а.** Пока `capacity() < maxBucketCapacity`, `Put` при нехватке места вызывает `rehashInPlace` — bucket того же `localDepth`, вдвое больше, без tombstone'ов. Когда предел достигнут, вызывается `splitBucket`: если `localDepth` bucket'а уже равен `globalDepth` директории, директория сперва удваивается (`growDirectory`), затем bucket делится на `lo`/`hi` по значению бита хэша на позиции `64 - newDepth` — первого бита, ещё не использованного директорией. Оба новых bucket'а получают `localDepth + 1`, а во все ячейки директории, ссылавшиеся на старый bucket, записываются `lo` или `hi` в зависимости от значения этого бита.
+
+### Использование
+
+```go
+m := NewFastSwissMap[string, int](hashString)
+m.Put("a", 1)
+m.Put("b", 2)
+val, ok := m.Get("a") // val = 1, ok = true
+m.Delete("b")
+n := m.Len() // n = 1
+```
+
+Функция хэширования передаётся снаружи (`hash func(K) uint64`) — таблица не привязана к конкретной хэш-функции.
+
+### Операции
+
+| Операция | Сложность | Описание |
+|---|---|---|
+| `Get(key)` | O(1) амортизированно | вернуть значение по ключу |
+| `Put(key, val)` | O(1) амортизированно | вставить или обновить значение |
+| `Delete(key)` | O(1) амортизированно | удалить запись по ключу |
+| `Len()` | O(1) | текущее число элементов |
+
+Максимальная средняя загрузка группы — `maxAvgGroupLoad = 7` из 8 слотов (~87.5%), после чего `growthLeft` обнуляется и bucket растёт.
+
+### Сборка и тестирование
+
+```bash
+go test -v ./...
+```
+
+---
+
+## English
+
+FastSwissMap is a general-purpose hash map combining two ideas: swiss table (batched lookup over control bytes within groups of 8 slots) and extendible hashing (a directory of bucket pointers that grows by splitting, not by a full rehash).
+
+A plain swiss table rehashes everything once it overflows. Here, each bucket is its own small swiss table instead. As long as a bucket is below `maxBucketCapacity` (64 slots), it just doubles in place. Once the limit is hit, the bucket splits into two along a single hash bit, and only that one bucket's entries get rehashed — not the whole map.
+
+### Internal layout
+
+**Control bytes and groups.** Each group occupies one `uint64` (`groupSize = 8`, one byte per slot). An empty slot is encoded as `0b10000000`, a deleted one (tombstone) as `0b11111110`. Matching a byte within a group is done by `matchByte` — a SWAR trick (broadcast XOR, subtract, mask the sign bits) that finds all matching positions at once instead of looping over eight bytes. The trick has a catch: subtracting the whole 64-bit word at once can produce a false positive from a borrow chain crossing byte boundaries, so candidates from the mask get re-verified byte by byte in `firstVerified`.
+
+**Hash split.** `splitHash` divides the 64-bit hash into `h1` (upper 57 bits — directory index plus the probe seed inside the bucket) and `h2` (lower 7 bits — what gets stored in the control byte). The directory index is the top `globalDepth` bits of the hash (`dirIndex`), so the same top-bit slice both selects the directory slot and decides which side of the split an entry lands on when a bucket is divided.
+
+**Probing.** Groups within a bucket are visited with quadratic probing — offsets grow by triangular numbers (0, 1, 3, 6, 10, ...) instead of incrementing by one each step, which avoids primary clustering. The sequence is a bijection modulo the group count, so the `probe < b.groups` loop is guaranteed to visit every group exactly once and terminate.
+
+**growthLeft.** A counter of genuinely empty (`ctrlEmpty`) slots. Deletion never refills it — a slot is marked either as a tombstone or, if its group was never fully occupied, directly as `ctrlEmpty`. If tombstones did refill growthLeft, a bucket could fill up with deleted bytes while the live entry count stayed low and never trigger growth: `Get`/`Put` would spin looking for an empty slot that no longer physically exists.
+
+**Deletion.** If the target slot's group already contains an empty byte, the group was never fully occupied and doesn't gate probe termination — the slot is downgraded straight to `ctrlEmpty` and `growthLeft` increases. Otherwise it becomes a tombstone: without it, probing for other keys whose chain passes through this slot would terminate too early.
+
+**Bucket growth.** While `capacity() < maxBucketCapacity`, a `Put` that runs out of room triggers `rehashInPlace` — a bucket of the same `localDepth`, twice the size, with tombstones cleared. Once the limit is reached, `splitBucket` runs: if the bucket's `localDepth` already equals the directory's `globalDepth`, the directory doubles first (`growDirectory`), then the bucket splits into `lo`/`hi` based on the hash bit at position `64 - newDepth` — the first bit not yet consumed by the directory. Both new buckets get `localDepth + 1`, and every directory slot that pointed at the old bucket is rewritten to `lo` or `hi` depending on that bit.
+
+### Usage
+
+```go
+m := NewFastSwissMap[string, int](hashString)
+m.Put("a", 1)
+m.Put("b", 2)
+val, ok := m.Get("a") // val = 1, ok = true
+m.Delete("b")
+n := m.Len() // n = 1
+```
+
+The hash function is supplied by the caller (`hash func(K) uint64`) — the table isn't tied to any specific hashing scheme.
+
+### Operations
+
+| Operation | Complexity | Description |
+|---|---|---|
+| `Get(key)` | O(1) amortized | return the value for a key |
+| `Put(key, val)` | O(1) amortized | insert or update a value |
+| `Delete(key)` | O(1) amortized | remove an entry by key |
+| `Len()` | O(1) | current number of elements |
+
+Maximum average group load is `maxAvgGroupLoad = 7` out of 8 slots (~87.5%), past which `growthLeft` hits zero and the bucket grows.
+
+### Build and test
+
+```bash
+go test -v ./...
+```
+
+---
+
+
+
+> Директория не хранит записи — она хранит право спросить нужный bucket. Когда bucket переполняется, делится не карта, а один-единственный узел ответственности.
+>
+> *The directory holds no entries of its own — only the right to ask the correct bucket. When a bucket overflows, what splits is not the map, but one single unit of responsibility.*
\ No newline at end of file
diff --git a/swiss_table/swiss_table.go b/swiss_table/swiss_table.go
new file mode 100644
index 0000000..1909398
--- /dev/null
+++ b/swiss_table/swiss_table.go
@@ -0,0 +1,464 @@
+package swiss_table
+
+import "math/bits"
+
+const (
+ groupSize = 8
+ ctrlEmpty = 0b10000000
+ ctrlDeleted = 0b11111110
+ maxAvgGroupLoad = 7
+ maxBucketCapacity = 64
+)
+
+type slot[K comparable, V any] struct {
+ key K
+ val V
+}
+
+func broadcast(b byte) uint64 { return uint64(b) * 0x0101010101010101 }
+
+func matchByte(word uint64, b byte) uint64 {
+ x := word ^ broadcast(b)
+ return (x - 0x0101010101010101) &^ x & 0x8080808080808080
+}
+
+func setByte(word uint64, pos int, b byte) uint64 {
+ shift := uint(pos) * 8
+ return (word &^ (0xFF << shift)) | (uint64(b) << shift)
+}
+
+func byteAt(word uint64, pos int) byte { return byte(word >> (uint(pos) * 8)) }
+
+// firstVerified: matchByte даёт ложные срабатывания (borrow-цепочка при вычитании
+// всего 64-битного слова разом), поэтому позиции из mask перепроверяются побайтово.
+// firstVerified: matchByte can false-positive (borrow chain from subtracting the whole
+// 64-bit word at once), so positions from mask are re-checked byte by byte.
+func firstVerified(word uint64, mask uint64, want byte) (pos int, ok bool) {
+ for mask != 0 {
+ p := bits.TrailingZeros64(mask) / 8
+ if byteAt(word, p) == want {
+ return p, true
+ }
+ mask &= mask - 1
+ }
+ return 0, false
+}
+
+// splitHash: h1 — верхние 57 бит (индекс в директории + затравка пробинга),
+// h2 — младшие 7 бит (кладутся в control-байт).
+// splitHash: h1 — upper 57 bits (directory index + probe seed),
+// h2 — lower 7 bits (stored as the control byte).
+func splitHash(hash uint64) (h1 uint64, h2 byte) {
+ return hash >> 7, byte(hash & 0x7F)
+}
+
+// dirIndex = hash >> (64-globalDepth): те же top globalDepth бит, что и в cockroachdb/swiss.
+// dirIndex = hash >> (64-globalDepth): same top-globalDepth-bits rule as cockroachdb/swiss.
+func dirIndex(hash uint64, globalDepth uint) uint64 {
+ if globalDepth == 0 {
+ return 0
+ }
+ return hash >> (64 - globalDepth)
+}
+
+// probeSeq — квадратичный пробинг между группами: смещения — треугольные числа
+// (0,1,3,6,10,...) вместо "+1", убирает первичную кластеризацию.
+// probeSeq — quadratic probing across groups: triangular-number offsets
+// (0,1,3,6,10,...) instead of "+1", removes primary clustering.
+type probeSeq struct {
+ mask uint64
+ offset uint64
+ index uint64
+}
+
+func makeProbeSeq(h1 uint64, mask uint64) probeSeq {
+ return probeSeq{mask: mask, offset: h1 & mask}
+}
+
+func (s probeSeq) next() probeSeq {
+ s.index++
+ s.offset = (s.offset + s.index) & s.mask
+ return s
+}
+
+// bucket — отдельная swiss-таблица, адресуется через extendible hashing.
+// Растёт удвоением до maxBucketCapacity, дальше — split на два bucket'а
+// с localDepth+1 вместо рехэша всей карты.
+// bucket — a standalone swiss table, addressed via extendible hashing.
+// Doubles in place up to maxBucketCapacity, then splits into two buckets
+// with localDepth+1 instead of rehashing the whole map.
+type bucket[K comparable, V any] struct {
+ ctrl []uint64
+ slots []slot[K, V]
+ groups int
+ used int
+
+ // growthLeft — число ещё доступных НАСТОЯЩИХ empty-слотов. Tombstone его
+ // не восполняет: иначе bucket может физически забиться deleted-байтами
+ // при малом used и никогда не вырасти → Get/Put зациклятся, не найдя ни
+ // одного ctrlEmpty.
+ // growthLeft — count of remaining REAL empty slots. Tombstones don't refill it:
+ // otherwise a bucket can fill up with deleted bytes while used stays low and
+ // never grow → Get/Put would loop forever finding no ctrlEmpty.
+ growthLeft int
+ localDepth uint
+}
+
+func newBucket[K comparable, V any](groups int, localDepth uint) *bucket[K, V] {
+ b := &bucket[K, V]{
+ ctrl: make([]uint64, groups),
+ slots: make([]slot[K, V], groups*groupSize),
+ groups: groups,
+ localDepth: localDepth,
+ }
+ b.growthLeft = (b.capacity() * maxAvgGroupLoad) / groupSize
+ empty := broadcast(ctrlEmpty)
+ for i := range b.ctrl {
+ b.ctrl[i] = empty
+ }
+ return b
+}
+
+func (b *bucket[K, V]) capacity() int { return b.groups * groupSize }
+
+func (b *bucket[K, V]) get(h1 uint64, h2 byte, key K) (val V, ok bool) {
+ seq := makeProbeSeq(h1, uint64(b.groups-1))
+ // probe < b.groups — явная граница: квадратичная последовательность по модулю
+ // степени двойки обходит каждую группу ровно раз за b.groups шагов, поэтому
+ // цикл гарантированно конечен. Страховка на случай нарушения инварианта growthLeft.
+
+ // probe < b.groups — explicit bound: quadratic probing modulo a power of two
+ // visits every group exactly once in b.groups steps, so the loop always
+ // terminates. Safety net in case the growthLeft invariant is ever violated.
+ for probe := 0; probe < b.groups; probe++ {
+ word := b.ctrl[seq.offset]
+
+ for matches := matchByte(word, h2); matches != 0; matches &= matches - 1 {
+ pos := bits.TrailingZeros64(matches) / 8
+ idx := int(seq.offset)*groupSize + pos
+ if b.slots[idx].key == key {
+ return b.slots[idx].val, true
+ }
+ }
+ if empty := matchByte(word, ctrlEmpty); empty != 0 {
+ if _, ok := firstVerified(word, empty, ctrlEmpty); ok {
+ var zero V
+ return zero, false
+ }
+ }
+ seq = seq.next()
+ }
+ var zero V
+ return zero, false
+}
+
+// putIfPresent обновляет значение существующего ключа; false, если дошли до
+// пустого слота (ключа в bucket'е нет).
+// putIfPresent updates the value of an existing key; false once an empty slot
+// is reached (key not present).
+func (b *bucket[K, V]) putIfPresent(h1 uint64, h2 byte, key K, val V) bool {
+ seq := makeProbeSeq(h1, uint64(b.groups-1))
+ for probe := 0; probe < b.groups; probe++ {
+ word := b.ctrl[seq.offset]
+
+ for matches := matchByte(word, h2); matches != 0; matches &= matches - 1 {
+ pos := bits.TrailingZeros64(matches) / 8
+ idx := int(seq.offset)*groupSize + pos
+ if b.slots[idx].key == key {
+ b.slots[idx].val = val
+ return true
+ }
+ }
+ if empty := matchByte(word, ctrlEmpty); empty != 0 {
+ if _, ok := firstVerified(word, empty, ctrlEmpty); ok {
+ return false
+ }
+ }
+ seq = seq.next()
+ }
+ return false
+}
+
+// insert вставляет новую пару, предполагая, что ключа ещё нет (проверка — на
+// вызывающей стороне). false = свободного слота нет, bucket нужно расщепить/растить.
+// insert adds a new pair assuming the key is absent (caller's responsibility to check).
+// false = no free slot, bucket must be grown or split.
+func (b *bucket[K, V]) insert(h1 uint64, h2 byte, key K, val V) bool {
+ seq := makeProbeSeq(h1, uint64(b.groups-1))
+ delGroup, delPos := -1, -1
+
+ for probe := 0; probe < b.groups; probe++ {
+ g := int(seq.offset)
+ word := b.ctrl[g]
+
+ if del := matchByte(word, ctrlDeleted); del != 0 && delGroup == -1 {
+ if p, ok := firstVerified(word, del, ctrlDeleted); ok {
+ delGroup, delPos = g, p
+ }
+ }
+
+ if empty := matchByte(word, ctrlEmpty); empty != 0 {
+ if p, ok := firstVerified(word, empty, ctrlEmpty); ok {
+ tg, tp := g, p
+ if delGroup != -1 {
+ tg, tp = delGroup, delPos
+ }
+ idx := tg*groupSize + tp
+ if delGroup == -1 {
+ b.growthLeft--
+ }
+ b.ctrl[tg] = setByte(b.ctrl[tg], tp, h2)
+ b.slots[idx] = slot[K, V]{key: key, val: val}
+ b.used++
+ return true
+ }
+ }
+ seq = seq.next()
+ }
+ return false
+}
+
+// delete: если в группе уже есть empty-байт, группа никогда не была полностью
+// заполненной и не участвует в обрыве цепочки пробинга — слот сразу помечается
+// empty (downgrade), иначе ставится tombstone, чтобы не сломать пробинг для
+// ключей, чья цепочка проходит через этот слот.
+// delete: if the group already has an empty byte, it was never fully occupied
+// and doesn't gate probe termination — the slot is downgraded straight to empty;
+// otherwise it becomes a tombstone so probing for other keys through this slot
+// still works.
+func (b *bucket[K, V]) delete(h1 uint64, h2 byte, key K) bool {
+ seq := makeProbeSeq(h1, uint64(b.groups-1))
+ for probe := 0; probe < b.groups; probe++ {
+ g := int(seq.offset)
+ word := b.ctrl[g]
+
+ for matches := matchByte(word, h2); matches != 0; matches &= matches - 1 {
+ pos := bits.TrailingZeros64(matches) / 8
+ idx := g*groupSize + pos
+ if b.slots[idx].key == key {
+ if empty := matchByte(word, ctrlEmpty); empty != 0 {
+ if _, ok := firstVerified(word, empty, ctrlEmpty); ok {
+ b.ctrl[g] = setByte(word, pos, ctrlEmpty)
+ b.growthLeft++
+ } else {
+ b.ctrl[g] = setByte(word, pos, ctrlDeleted)
+ }
+ } else {
+ b.ctrl[g] = setByte(word, pos, ctrlDeleted)
+ }
+ var zero slot[K, V]
+ b.slots[idx] = zero
+ b.used--
+ return true
+ }
+ }
+ if empty := matchByte(word, ctrlEmpty); empty != 0 {
+ if _, ok := firstVerified(word, empty, ctrlEmpty); ok {
+ return false
+ }
+ }
+ seq = seq.next()
+ }
+ return false
+}
+
+// rehashInPlace переносит живые записи в новый bucket той же localDepth, вдвое
+// больше — убирает tombstone'ы без расщепления по биту хэша.
+// rehashInPlace copies live entries into a new bucket, same localDepth, double
+// size — clears tombstones without splitting on a hash bit.
+func (b *bucket[K, V]) rehashInPlace(hash func(K) uint64) *bucket[K, V] {
+ grown := newBucket[K, V](b.groups*2, b.localDepth)
+ for g := 0; g < b.groups; g++ {
+ word := b.ctrl[g]
+ for pos := 0; pos < groupSize; pos++ {
+ c := byteAt(word, pos)
+ if c != ctrlEmpty && c != ctrlDeleted {
+ idx := g*groupSize + pos
+ h1, h2 := splitHash(hash(b.slots[idx].key))
+ grown.insert(h1, h2, b.slots[idx].key, b.slots[idx].val)
+ }
+ }
+ }
+ return grown
+}
+
+// split делит bucket на два с localDepth+1 по значению бита hash на позиции
+// bitPos = 64-newDepth (первый ещё не использованный директорией бит).
+// split partitions a bucket into two with localDepth+1, keyed on the hash bit
+// at bitPos = 64-newDepth (the first bit not yet consumed by the directory).
+func (b *bucket[K, V]) split(hash func(K) uint64) (lo, hi *bucket[K, V]) {
+ newDepth := b.localDepth + 1
+ groups := b.groups
+ if groups < 1 {
+ groups = 1
+ }
+ lo = newBucket[K, V](groups, newDepth)
+ hi = newBucket[K, V](groups, newDepth)
+
+ bitPos := 64 - newDepth
+ for g := 0; g < b.groups; g++ {
+ word := b.ctrl[g]
+ for pos := 0; pos < groupSize; pos++ {
+ c := byteAt(word, pos)
+ if c != ctrlEmpty && c != ctrlDeleted {
+ idx := g*groupSize + pos
+ key, val := b.slots[idx].key, b.slots[idx].val
+ full := hash(key)
+ h1, h2 := splitHash(full)
+ dst := lo
+ if (full>>bitPos)&1 == 1 {
+ dst = hi
+ }
+ if !dst.insert(h1, h2, key, val) {
+ // dst сам переполнился при перераспределении — растим его на месте.
+ // dst overflowed during redistribution — grow it in place.
+ dst = dst.rehashInPlace(hash)
+ dst.insert(h1, h2, key, val)
+ }
+ if (full>>bitPos)&1 == 1 {
+ hi = dst
+ } else {
+ lo = dst
+ }
+ }
+ }
+ }
+ return lo, hi
+}
+
+// FastSwissMap — swiss table с квадратичным пробингом и extendible hashing:
+// рост = split одного переполненного bucket'а, а не рехэш всей карты.
+// FastSwissMap — swiss table with quadratic probing and extendible hashing:
+// growth means splitting one overflowing bucket, not rehashing the whole map.
+type FastSwissMap[K comparable, V any] struct {
+ dir []*bucket[K, V]
+ globalDepth uint
+ count int
+ hash func(K) uint64
+}
+
+func NewFastSwissMap[K comparable, V any](hash func(K) uint64) *FastSwissMap[K, V] {
+ b := newBucket[K, V](1, 0)
+ return &FastSwissMap[K, V]{
+ dir: []*bucket[K, V]{b},
+ hash: hash,
+ }
+}
+
+func (m *FastSwissMap[K, V]) bucketFor(hash uint64) (idx uint64, b *bucket[K, V]) {
+ idx = dirIndex(hash, m.globalDepth)
+ return idx, m.dir[idx]
+}
+
+func (m *FastSwissMap[K, V]) Get(key K) (V, bool) {
+ full := m.hash(key)
+ h1, h2 := splitHash(full)
+ _, b := m.bucketFor(full)
+ return b.get(h1, h2, key)
+}
+
+func (m *FastSwissMap[K, V]) Put(key K, val V) {
+ full := m.hash(key)
+ h1, h2 := splitHash(full)
+
+ idx, b := m.bucketFor(full)
+ if b.putIfPresent(h1, h2, key, val) {
+ return
+ }
+
+ // growthLeft>0 гарантирует хотя бы один настоящий empty где-то в bucket'е —
+ // insert() пройдёт по всем группам (probeSeq — биекция по modulo) и найдёт его.
+
+ // growthLeft>0 guarantees at least one real empty somewhere in the bucket —
+ // insert() sweeps all groups (probeSeq is a bijection mod groups) and finds it.
+ if b.growthLeft > 0 {
+ if b.insert(h1, h2, key, val) {
+ m.count++
+ return
+ }
+ }
+
+ if b.capacity() < maxBucketCapacity {
+ // Не достигли предела bucket'а — растим удвоением на месте (заодно чистим tombstone'ы).
+ // Below the bucket size limit — grow in place by doubling (also clears tombstones).
+ grown := b.rehashInPlace(m.hash)
+ m.installBucket(idx, grown)
+ grown.insert(h1, h2, key, val)
+ m.count++
+ return
+ }
+
+ // Bucket достиг maxBucketCapacity — расщепляем вместо рехэша всей карты.
+ // Bucket hit maxBucketCapacity — split instead of rehashing the whole map.
+ m.splitBucket(idx, b)
+ m.Put(key, val) // директория и bucket изменились — вставляем заново / directory and bucket changed — retry insertion
+}
+
+func (m *FastSwissMap[K, V]) Delete(key K) bool {
+ full := m.hash(key)
+ h1, h2 := splitHash(full)
+ _, b := m.bucketFor(full)
+ if b.delete(h1, h2, key) {
+ m.count--
+ return true
+ }
+ return false
+}
+
+func (m *FastSwissMap[K, V]) Len() int { return m.count }
+
+// installBucket пишет bucket во все 2^(globalDepth-localDepth) ячеек директории,
+// которые на него указывают.
+// installBucket writes the bucket into all 2^(globalDepth-localDepth) directory
+// slots that reference it.
+func (m *FastSwissMap[K, V]) installBucket(anyIdx uint64, b *bucket[K, V]) {
+ step := uint64(1) << (m.globalDepth - b.localDepth)
+ start := (anyIdx / step) * step
+ for i := uint64(0); i < step; i++ {
+ m.dir[start+i] = b
+ }
+}
+
+// splitBucket расщепляет переполненный bucket на два. Если его localDepth уже
+// равна globalDepth, директория сначала удваивается.
+// splitBucket splits an overflowing bucket into two. If its localDepth already
+// equals globalDepth, the directory is doubled first.
+func (m *FastSwissMap[K, V]) splitBucket(idx uint64, b *bucket[K, V]) {
+ // origLocalDepth — глубина ДО расщепления: задаёт суммарное число ячеек
+ // директории для lo+hi. Глубина уже созданных lo/hi (localDepth+1) для этого
+ // не годится — с ней часть ячеек останется не переписана и укажет на
+ // выброшенный bucket (был баг именно на этом месте).
+
+ // origLocalDepth — depth BEFORE the split: defines the total directory slot
+ // count for lo+hi combined. Using the already-incremented lo/hi depth here is
+ // wrong — some slots would stay unwritten and point at the discarded bucket
+ // (this exact spot was the bug).
+ origLocalDepth := b.localDepth
+ if origLocalDepth == m.globalDepth {
+ m.growDirectory()
+ // Директория удвоилась: старый индекс i -> пара новых {2i, 2i+1}.
+ // Directory doubled: old index i -> new pair {2i, 2i+1}.
+ idx *= 2
+ }
+ lo, hi := b.split(m.hash)
+
+ step := uint64(1) << (m.globalDepth - origLocalDepth)
+ start := (idx / step) * step
+ half := step / 2
+ for i := uint64(0); i < half; i++ {
+ m.dir[start+i] = lo
+ }
+ for i := half; i < step; i++ {
+ m.dir[start+i] = hi
+ }
+}
+
+func (m *FastSwissMap[K, V]) growDirectory() {
+ newDir := make([]*bucket[K, V], len(m.dir)*2)
+ for i, b := range m.dir {
+ newDir[2*i] = b
+ newDir[2*i+1] = b
+ }
+ m.dir = newDir
+ m.globalDepth++
+}
diff --git a/swiss_table/swiss_table_test.go b/swiss_table/swiss_table_test.go
new file mode 100644
index 0000000..295fbe9
--- /dev/null
+++ b/swiss_table/swiss_table_test.go
@@ -0,0 +1,475 @@
+package swiss_table
+
+import (
+ "fmt"
+ "math/bits"
+ "math/rand"
+ "testing"
+ "time"
+)
+
+func hashInt(k int) uint64 {
+ h := uint64(k)
+ h ^= h >> 33
+ h *= 0xff51afd7ed558ccd
+ h ^= h >> 33
+ h *= 0xc4ceb9fe1a85ec53
+ h ^= h >> 33
+ return h
+}
+
+func TestEmptyMap(t *testing.T) {
+ m := NewFastSwissMap[int, int](hashInt)
+ if m.Len() != 0 {
+ t.Fatalf("Len() = %d, want 0", m.Len())
+ }
+ if _, ok := m.Get(42); ok {
+ t.Fatalf("Get on empty map returned ok=true")
+ }
+ if m.Delete(42) {
+ t.Fatalf("Delete on empty map returned true")
+ }
+}
+
+func TestPutGetSingle(t *testing.T) {
+ m := NewFastSwissMap[int, string](hashInt)
+ m.Put(1, "one")
+ v, ok := m.Get(1)
+ if !ok || v != "one" {
+ t.Fatalf("Get(1) = (%q,%v), want (\"one\",true)", v, ok)
+ }
+ if m.Len() != 1 {
+ t.Fatalf("Len() = %d, want 1", m.Len())
+ }
+}
+
+func TestPutGetMany(t *testing.T) {
+ m := NewFastSwissMap[int, int](hashInt)
+ const n = 1000
+ for i := 0; i < n; i++ {
+ m.Put(i, i*i)
+ }
+ if m.Len() != n {
+ t.Fatalf("Len() = %d, want %d", m.Len(), n)
+ }
+ for i := 0; i < n; i++ {
+ v, ok := m.Get(i)
+ if !ok || v != i*i {
+ t.Fatalf("Get(%d) = (%d,%v), want (%d,true)", i, v, ok, i*i)
+ }
+ }
+}
+
+func TestOverwriteDoesNotGrowLen(t *testing.T) {
+ m := NewFastSwissMap[int, int](hashInt)
+ m.Put(42, 1)
+ m.Put(42, 2)
+ m.Put(42, 3)
+ if v, ok := m.Get(42); !ok || v != 3 {
+ t.Fatalf("Get(42) = (%d,%v), want (3,true)", v, ok)
+ }
+ if m.Len() != 1 {
+ t.Fatalf("Len() = %d, want 1 (overwrite must not add a new entry)", m.Len())
+ }
+}
+
+func TestGetMissingKey(t *testing.T) {
+ m := NewFastSwissMap[int, int](hashInt)
+ for i := 0; i < 100; i++ {
+ m.Put(i, i)
+ }
+ for _, k := range []int{-1, 100, 100000, -100000} {
+ if _, ok := m.Get(k); ok {
+ t.Fatalf("Get(%d) = ok=true, want false (key was never inserted)", k)
+ }
+ }
+}
+
+func TestDeleteReducesLen(t *testing.T) {
+ m := NewFastSwissMap[int, int](hashInt)
+ for i := 0; i < 10; i++ {
+ m.Put(i, i)
+ }
+ if !m.Delete(5) {
+ t.Fatalf("Delete(5) = false, want true")
+ }
+ if m.Len() != 9 {
+ t.Fatalf("Len() = %d, want 9", m.Len())
+ }
+ if _, ok := m.Get(5); ok {
+ t.Fatalf("Get(5) after Delete returned ok=true")
+ }
+
+ for _, k := range []int{4, 6} {
+ if v, ok := m.Get(k); !ok || v != k {
+ t.Fatalf("Get(%d) = (%d,%v), want (%d,true)", k, v, ok, k)
+ }
+ }
+}
+
+func TestDeleteMissingKeyIsNoop(t *testing.T) {
+ m := NewFastSwissMap[int, int](hashInt)
+ m.Put(1, 1)
+ if m.Delete(999) {
+ t.Fatalf("Delete(999) = true, want false (key was never present)")
+ }
+ if m.Len() != 1 {
+ t.Fatalf("Len() = %d, want 1 (unaffected by no-op delete)", m.Len())
+ }
+}
+
+func TestDeleteThenReinsert(t *testing.T) {
+ m := NewFastSwissMap[int, int](hashInt)
+ const n = 500
+ for i := 0; i < n; i++ {
+ m.Put(i, i)
+ }
+ for i := 0; i < n; i += 2 {
+ if !m.Delete(i) {
+ t.Fatalf("Delete(%d) = false, want true", i)
+ }
+ }
+ if m.Len() != n/2 {
+ t.Fatalf("Len() = %d, want %d", m.Len(), n/2)
+ }
+
+ for i := 0; i < n; i += 2 {
+ m.Put(i, i*10)
+ }
+ if m.Len() != n {
+ t.Fatalf("Len() = %d, want %d", m.Len(), n)
+ }
+ for i := 0; i < n; i++ {
+ want := i
+ if i%2 == 0 {
+ want = i * 10
+ }
+ v, ok := m.Get(i)
+ if !ok || v != want {
+ t.Fatalf("Get(%d) = (%d,%v), want (%d,true)", i, v, ok, want)
+ }
+ }
+}
+
+func TestZeroValueKeysAndValues(t *testing.T) {
+ m := NewFastSwissMap[int, int](hashInt)
+ m.Put(0, 0)
+ v, ok := m.Get(0)
+ if !ok || v != 0 {
+ t.Fatalf("Get(0) = (%d,%v), want (0,true) — zero key/value must be distinguishable from absence", v, ok)
+ }
+ if !m.Delete(0) {
+ t.Fatalf("Delete(0) = false, want true")
+ }
+ if _, ok := m.Get(0); ok {
+ t.Fatalf("Get(0) after delete = ok=true, want false")
+ }
+}
+
+func TestStringKeys(t *testing.T) {
+ hashStr := func(s string) uint64 {
+ var h uint64 = 14695981039346656037
+ for i := 0; i < len(s); i++ {
+ h ^= uint64(s[i])
+ h *= 1099511628211
+ }
+ return h
+ }
+ m := NewFastSwissMap[string, int](hashStr)
+ words := []string{"", "a", "ab", "abc", "swiss", "table", "quadratic", "probing"}
+ for i, w := range words {
+ m.Put(w, i)
+ }
+ for i, w := range words {
+ v, ok := m.Get(w)
+ if !ok || v != i {
+ t.Fatalf("Get(%q) = (%d,%v), want (%d,true)", w, v, ok, i)
+ }
+ }
+ if m.Len() != len(words) {
+ t.Fatalf("Len() = %d, want %d", m.Len(), len(words))
+ }
+}
+
+func TestRehashInPlaceBelowCapacityLimit(t *testing.T) {
+ m := NewFastSwissMap[int, int](hashInt)
+
+ const n = 40
+ for i := 0; i < n; i++ {
+ m.Put(i, i)
+ }
+ if m.globalDepth != 0 {
+ t.Fatalf("globalDepth = %d, want 0 (must not have split yet)", m.globalDepth)
+ }
+ if len(m.dir) != 1 {
+ t.Fatalf("len(dir) = %d, want 1", len(m.dir))
+ }
+ for i := 0; i < n; i++ {
+ if v, ok := m.Get(i); !ok || v != i {
+ t.Fatalf("Get(%d) = (%d,%v), want (%d,true)", i, v, ok, i)
+ }
+ }
+}
+
+func TestBucketSplitTriggers(t *testing.T) {
+ m := NewFastSwissMap[int, int](hashInt)
+ const n = 50_000
+ for i := 0; i < n; i++ {
+ m.Put(i, i)
+ }
+ if m.globalDepth == 0 {
+ t.Fatalf("globalDepth = 0, want > 0 after %d inserts (split should have triggered)", n)
+ }
+ if len(m.dir) != 1<