recursive-set 7.0.1 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/index.js CHANGED
@@ -1,554 +1,782 @@
1
1
  "use strict";
2
2
  /**
3
3
  * @module recursive-set
4
- * @version 7.0.0
4
+ * @version 8.1.0
5
5
  * @description
6
- * **High-Performance ZFC Set Implementation.**
7
- * This library sacrifices runtime safety checks for raw speed.
8
- * * **Strict Contract:**
9
- * 1. **Finite Numbers Only:** No `NaN`, no `Infinity`.
10
- * 2. **No Mutation:** Do not mutate arrays/tuples/objects after insertion.
11
- * 3. **Type Consistency:** Avoid mixing distinct types (Array vs Tuple) that might hash collide.
6
+ * High-Performance collection library supporting **Value Semantics** (Deep Equality)
7
+ * and recursive structures.
8
+ * * **CONTRACTS & INVARIANTS:**
9
+ * 1. **Finite Numbers Only:** `NaN` and `Infinity` are **strictly forbidden**. They break strict equality checks and integer optimization paths.
10
+ * 2. **Strict Value Semantics:** Plain JavaScript objects (`{}`) are **not supported**. Objects must implement the `Structural` interface.
11
+ * 3. **Hash Quality:** The $O(1)$ performance guarantee relies on a good distribution. Returning a constant `hashCode` (e.g. `42`) forces all elements into a single bucket, degrading performance to $O(N)$.
12
+ * 4. **Deterministic Visualization:** Custom `toString()` implementations **must** utilize `compareVisualLogic` for nested structures. Failing to do so results in cut/unstable string output.
13
+ * 5. **Immutability:** Once an object is added to a collection, its `hashCode` **must not change**.
14
+ * 6. **No Circular Dependencies:** A `RecursiveSet` cannot contain itself, directly or indirectly (e.g. A ∈ B ∈ A). Runtime checks are omitted for performance. Creating a cycle will cause a **Stack Overflow** during hashing or visualization.
15
+ * * **Architecture:**
16
+ * - **Storage:** Open Addressing with Linear Probing.
17
+ * - **Hashing:** Zero-allocation FNV-1a / Murmur-inspired hybrid.
18
+ * - **Memory:** Flattened arrays for cache locality (SoA - Structure of Arrays).
12
19
  */
13
20
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.RecursiveSet = exports.Tuple = void 0;
15
- exports.compare = compare;
21
+ exports.hashValue = exports.Tuple = exports.RecursiveMap = exports.RecursiveSet = void 0;
16
22
  exports.emptySet = emptySet;
17
23
  exports.singleton = singleton;
24
+ exports.getHashCode = getHashCode;
18
25
  // ============================================================================
19
- // FAST HASHING (Optimized FNV-1a)
26
+ // 2. VISUAL HELPERS
20
27
  // ============================================================================
21
- const FNV_PRIME = 16777619;
22
- const FNV_OFFSET = 2166136261;
23
- const floatBuffer = new ArrayBuffer(8);
24
- const view = new DataView(floatBuffer);
25
28
  /**
26
- * Hashes a number using FNV-1a.
27
- * Optimizes for 32-bit integers to avoid Float64 processing overhead.
29
+ * Visual Comparator used strictly for deterministic .toString() output.
30
+ * * @remarks
31
+ * This does NOT define the logical order for the Set/Map (which are unordered).
32
+ * It ensures that `{a, b}` and `{b, a}` produce the same string output.
28
33
  */
29
- function hashNumber(val) {
30
- // Integer optimization: Skip float logic if it's a 32-bit int
31
- if ((val | 0) === val)
32
- return val | 0;
33
- view.setFloat64(0, val, true); // Little Endian
34
- let h = FNV_OFFSET;
35
- h ^= view.getInt32(0, true);
36
- h = Math.imul(h, FNV_PRIME);
37
- h ^= view.getInt32(4, true);
38
- h = Math.imul(h, FNV_PRIME);
39
- return h >>> 0;
40
- }
41
- function hashString(str) {
42
- let h = FNV_OFFSET;
43
- const len = str.length;
44
- for (let i = 0; i < len; i++) {
45
- h ^= str.charCodeAt(i);
46
- h = Math.imul(h, FNV_PRIME);
47
- }
48
- return h >>> 0;
34
+ function compareVisualLogic(a, b) {
35
+ if (a === b)
36
+ return 0;
37
+ // 1. Numbers: Mathematical sort
38
+ if (typeof a === 'number' && typeof b === 'number')
39
+ return a - b;
40
+ // 2. Strings: Lexicographic sort
41
+ if (typeof a === 'string' && typeof b === 'string')
42
+ return a < b ? -1 : 1;
43
+ // 3. Type separation (Numbers < Strings < Objects)
44
+ const typeA = getTypeId(a);
45
+ const typeB = getTypeId(b);
46
+ if (typeA !== typeB)
47
+ return typeA - typeB;
48
+ // 4. Recursive Fallback: Compare string representations
49
+ return String(a) < String(b) ? -1 : 1;
49
50
  }
50
- function hashValue(v) {
51
+ function getTypeId(v) {
51
52
  if (typeof v === 'number')
52
- return hashNumber(v);
53
+ return 1;
53
54
  if (typeof v === 'string')
54
- return hashString(v);
55
- if (v instanceof RecursiveSet || v instanceof Tuple)
56
- return v.hashCode;
57
- if (Array.isArray(v)) {
58
- let h = FNV_OFFSET;
59
- for (let i = 0; i < v.length; i++) {
60
- h ^= hashValue(v[i]);
61
- h = Math.imul(h, FNV_PRIME);
55
+ return 2;
56
+ return 3; // Objects / Structures
57
+ }
58
+ // ============================================================================
59
+ // 3. HASH ENGINE (Optimized)
60
+ // ============================================================================
61
+ // Static allocation to prevent GC pauses during number hashing.
62
+ // We treat numbers as raw bits to avoid FPU overhead.
63
+ const _buffer = new ArrayBuffer(8);
64
+ const _f64 = new Float64Array(_buffer);
65
+ const _i32 = new Int32Array(_buffer);
66
+ // Mixing constants (derived from MurmurHash3)
67
+ const HASH_C1 = 0xcc9e2d51;
68
+ const HASH_C2 = 0x1b873593;
69
+ /**
70
+ * Computes a high-quality 32-bit hash code for any supported Value.
71
+ * Optimized for V8's internal number representation (Smis vs Doubles).
72
+ */
73
+ function getHashCode(val) {
74
+ // --- CASE A: NUMBER ---
75
+ if (typeof val === 'number') {
76
+ // Fast Path: 32-Bit Integers (Smis)
77
+ // (val | 0) === val checks if it's a safe integer.
78
+ // This handles -0 correctly (converts to 0).
79
+ if ((val | 0) === val) {
80
+ let h = val | 0;
81
+ // Avalanche Mixer: Spreads bits to prevent collisions on sequential numbers
82
+ h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
83
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
84
+ return (h ^ (h >>> 16)) >>> 0;
85
+ }
86
+ // Slow Path: Floats (Doubles)
87
+ // Interpret the double's IEEE 754 bit pattern as two 32-bit integers.
88
+ _f64[0] = val;
89
+ const low = _i32[0];
90
+ const high = _i32[1];
91
+ return (Math.imul(low, HASH_C1) ^ Math.imul(high, HASH_C2)) >>> 0;
92
+ }
93
+ // --- CASE B: STRING ---
94
+ // Jenkins One-at-a-Time variant
95
+ if (typeof val === 'string') {
96
+ let h = 0x811c9dc5;
97
+ const len = val.length;
98
+ for (let i = 0; i < len; i++) {
99
+ h ^= val.charCodeAt(i);
100
+ h = Math.imul(h, 0x01000193);
62
101
  }
63
102
  return h >>> 0;
64
103
  }
104
+ // --- CASE C: STRUCTURAL ---
105
+ if (val && typeof val === 'object') {
106
+ return val.hashCode;
107
+ }
65
108
  return 0;
66
109
  }
67
- // ============================================================================
68
- // COMPARATOR (Optimized)
69
- // ============================================================================
70
110
  /**
71
- * Global comparator for Total Ordering.
72
- * * **WARNING: UNSAFE OPTIMIZATIONS**
73
- * - Uses `a - b` for numbers. **Precondition:** Only finite numbers allowed.
74
- * Inputting `NaN` or `Infinity` results in undefined sorting behavior.
75
- * - **Hash Collisions:** If two distinct object types (e.g. Array vs Tuple) have the
76
- * same hash, they may be treated as equal. Avoid mixing structure types in the same set.
111
+ * Deep equality check dispatcher.
77
112
  */
78
- function compare(a, b) {
113
+ function areEqual(a, b) {
79
114
  if (a === b)
80
- return 0;
81
- // Fast path for primitives
82
- if (typeof a === 'number' && typeof b === 'number')
83
- return a - b;
84
- if (typeof a === 'string' && typeof b === 'string')
85
- return a < b ? -1 : 1;
86
- const typeA = typeof a;
87
- const typeB = typeof b;
88
- // Type Score: Number(1) < String(2) < Array/Tuple(3) < Set(4)
89
- if (typeA !== typeB) {
90
- const scoreA = (typeA === 'number') ? 1 : (typeA === 'string' ? 2 : 3);
91
- const scoreB = (typeB === 'number') ? 1 : (typeB === 'string' ? 2 : 3);
92
- return scoreA - scoreB;
93
- }
94
- // Hash Short-Circuit
95
- const h1 = hashValue(a);
96
- const h2 = hashValue(b);
97
- if (h1 !== h2)
98
- return h1 < h2 ? -1 : 1;
99
- // Deep Compare
100
- if (a instanceof RecursiveSet && b instanceof RecursiveSet)
101
- return a.compare(b);
102
- if (a instanceof Tuple && b instanceof Tuple)
103
- return compareSequences(a.raw, b.raw);
104
- if (Array.isArray(a) && Array.isArray(b))
105
- return compareSequences(a, b);
106
- return 0;
107
- }
108
- function compareSequences(a, b) {
109
- const len = a.length;
110
- if (len !== b.length)
111
- return len - b.length;
112
- for (let i = 0; i < len; i++) {
113
- const diff = compare(a[i], b[i]);
114
- if (diff !== 0)
115
- return diff;
115
+ return true;
116
+ if (typeof a !== typeof b)
117
+ return false;
118
+ if (typeof a === 'number' || typeof a === 'string')
119
+ return a === b;
120
+ if (typeof a === 'object' && a !== null && b !== null) {
121
+ return a.equals(b);
116
122
  }
117
- return 0;
123
+ return false;
118
124
  }
119
125
  // ============================================================================
120
- // CLASSES
126
+ // 4. TUPLE
121
127
  // ============================================================================
122
128
  /**
123
- * Immutable Tuple container.
124
- * * **Contract:**
125
- * - Creates a defensive copy of the input array.
126
- * - Freezes the internal storage (`Object.freeze`).
127
- * - **Note:** Freezing is **shallow**. Do not mutate nested elements.
128
- * @template T - Array type of the tuple elements.
129
+ * An immutable, hashable sequence of values.
130
+ * Useful as composite keys in Maps.
129
131
  */
130
132
  class Tuple {
131
- #values;
132
- hashCode;
133
- constructor(...values) {
134
- this.#values = values.slice(); // Defensive copy
135
- Object.freeze(this.#values); // Freeze for safety
136
- this.hashCode = hashValue(this.#values);
137
- }
138
- /** * Returns the readonly internal array.
139
- * **Warning:** Readonly is only enforced by TypeScript.
140
- * Mutating the underlying array via `as any` breaks invariants.
141
- */
142
- get raw() { return this.#values; }
143
- get length() { return this.#values.length; }
144
- /** Alias for compatibility. */
145
- get values() { return this.#values; }
146
- /** Iterates over tuple elements. */
147
- *[Symbol.iterator]() { yield* this.#values; }
148
- /** Returns string representation "(a, b)". */
149
- toString() { return `(${this.#values.join(', ')})`; }
150
- /** Custom inspection for Node.js console.log to print "(a, b)" cleanly. */
133
+ #elements;
134
+ #hashCode;
135
+ constructor(...elements) {
136
+ this.#elements = [...elements]; // Defensive copy
137
+ Object.freeze(this.#elements);
138
+ // Compute hash immediately (tuples are immutable)
139
+ let h = 1;
140
+ for (const e of this.#elements) {
141
+ h = Math.imul(h, 31) + getHashCode(e);
142
+ }
143
+ this.#hashCode = h;
144
+ }
145
+ get length() { return this.#elements.length; }
146
+ get hashCode() { return this.#hashCode; }
147
+ /** Typesafe access to elements */
148
+ get(index) {
149
+ return this.#elements[index];
150
+ }
151
+ equals(other) {
152
+ if (this === other)
153
+ return true;
154
+ if (!(other instanceof Tuple))
155
+ return false;
156
+ if (this.hashCode !== other.hashCode)
157
+ return false;
158
+ if (this.length !== other.length)
159
+ return false;
160
+ for (let i = 0; i < this.length; i++) {
161
+ if (!areEqual(this.#elements[i], other.#elements[i]))
162
+ return false;
163
+ }
164
+ return true;
165
+ }
166
+ [Symbol.iterator]() {
167
+ return this.#elements[Symbol.iterator]();
168
+ }
169
+ toString() {
170
+ return `(${this.#elements.map(e => String(e)).join(', ')})`;
171
+ }
151
172
  [Symbol.for('nodejs.util.inspect.custom')]() { return this.toString(); }
152
173
  }
153
174
  exports.Tuple = Tuple;
175
+ // ============================================================================
176
+ // 5. RECURSIVE MAP
177
+ // ============================================================================
154
178
  /**
155
- * High-Performance Recursive Set.
156
- * * **Lifecycle & Safety:**
157
- * - **Freeze-on-Hash:** The set is effectively immutable once `hashCode` is accessed
158
- * (or it is added to another set/map).
159
- * - **Runtime Checks:** Mutation methods verify frozen state via a fast boolean check.
160
- * * @template T - Type of elements.
179
+ * A Hash Map implementation supporting complex keys (Value Semantics).
180
+ * * @template K Key Type
181
+ * @template V Value Type
161
182
  */
162
- class RecursiveSet {
163
- #elements;
164
- #hashCode = null;
165
- #isFrozen = false;
166
- // Static wrapper for compatibility
167
- static compare(a, b) { return compare(a, b); }
183
+ class RecursiveMap {
184
+ // Structure of Arrays (SoA) for cache locality
185
+ _keys = [];
186
+ _values = [];
187
+ _hashes = [];
188
+ // Open Addressing Index Table
189
+ // Maps (Hash & Mask) -> Index in _keys/_values + 1 (0 means empty)
190
+ _indices;
191
+ _bucketCount = 16;
192
+ _mask = 15;
193
+ _isFrozen = false;
194
+ _cachedHash = null;
195
+ constructor() {
196
+ this._indices = new Uint32Array(16);
197
+ }
198
+ get size() { return this._keys.length; }
199
+ get isFrozen() { return this._isFrozen; }
200
+ isEmpty() { return this._keys.length === 0; }
168
201
  /**
169
- * Creates a new RecursiveSet.
170
- * Elements are sorted and deduplicated ($O(N \log N)$).
171
- * @param elements Initial elements.
202
+ * Creates a shallow mutable copy of the map.
203
+ * Useful when the current map is frozen.
172
204
  */
173
- constructor(...elements) {
174
- if (elements.length > 1) {
175
- elements.sort(compare);
176
- this.#elements = this.#unique(elements);
177
- }
178
- else {
179
- this.#elements = elements;
205
+ mutableCopy() {
206
+ const copy = new RecursiveMap();
207
+ copy.ensureCapacity(this.size);
208
+ for (let i = 0; i < this._keys.length; i++) {
209
+ copy.set(this._keys[i], this._values[i]);
180
210
  }
211
+ return copy;
181
212
  }
182
- // === CRITICAL PERFORMANCE METHODS ===
183
- /** * **Bulk Load (O(N log N))**: Creates a set from a raw array.
184
- * Sorts and deduplicates. Much faster than iterative insertion.
213
+ /**
214
+ * Computes the hash code of the map.
215
+ * Order-independent (XOR sum).
216
+ * @remarks ACCESSING THIS FREEZES THE MAP.
185
217
  */
186
- static fromArray(elements) {
187
- const s = new RecursiveSet();
188
- if (elements.length > 1) {
189
- elements.sort(compare);
190
- s.#elements = s.#unique(elements);
191
- }
192
- else {
193
- s.#elements = elements;
218
+ get hashCode() {
219
+ if (this._cachedHash !== null)
220
+ return this._cachedHash;
221
+ let h = 0;
222
+ for (let i = 0; i < this._keys.length; i++) {
223
+ const k = this._keys[i];
224
+ const hk = this._hashes[i]; // Use cached hash for speed
225
+ // Force freeze on the key if it's structural (nested freeze)
226
+ if (k && typeof k === 'object') {
227
+ k.hashCode;
228
+ }
229
+ // Calculate value hash (freezes value if structural)
230
+ const hv = getHashCode(this._values[i]);
231
+ // Mix key hash and value hash
232
+ h ^= Math.imul(hk, 31) ^ hv;
194
233
  }
195
- return s;
234
+ this._cachedHash = h;
235
+ this._isFrozen = true;
236
+ return h;
196
237
  }
197
- /** * **UNSAFE (O(1))**: Bypasses all checks.
198
- * @param sortedUnique Input must be ALREADY sorted and deduplicated.
199
- * Use only if you strictly guarantee invariants.
238
+ /**
239
+ * Resizes the internal lookup table if load factor > 0.75.
200
240
  */
201
- static fromSortedUnsafe(sortedUnique) {
202
- const s = new RecursiveSet();
203
- s.#elements = sortedUnique;
204
- return s;
241
+ ensureCapacity(capacity) {
242
+ if (capacity * 1.33 > this._bucketCount) {
243
+ let target = this._bucketCount;
244
+ while (target * 0.75 < capacity)
245
+ target *= 2;
246
+ this._bucketCount = target;
247
+ this._mask = this._bucketCount - 1;
248
+ // Rehash all entries into new index table
249
+ const oldKeys = this._keys;
250
+ const oldHashes = this._hashes;
251
+ this._indices = new Uint32Array(this._bucketCount);
252
+ for (let i = 0; i < oldKeys.length; i++) {
253
+ const h = oldHashes[i];
254
+ let idx = h & this._mask;
255
+ // Linear Probing
256
+ while (this._indices[idx] !== 0)
257
+ idx = (idx + 1) & this._mask;
258
+ this._indices[idx] = i + 1;
259
+ }
260
+ }
261
+ }
262
+ resize() {
263
+ this.ensureCapacity(this._keys.length + 1);
205
264
  }
206
- #unique(sorted) {
207
- if (sorted.length < 2)
208
- return sorted;
209
- const out = [sorted[0]];
210
- let last = sorted[0];
211
- const len = sorted.length;
212
- for (let i = 1; i < len; i++) {
213
- const curr = sorted[i];
214
- if (compare(curr, last) !== 0) {
215
- out.push(curr);
216
- last = curr;
265
+ /**
266
+ * Associates the specified value with the specified key.
267
+ * If the map previously contained a mapping for the key, the old value is replaced.
268
+ */
269
+ set(key, value) {
270
+ if (this._isFrozen)
271
+ throw new Error("Frozen Map");
272
+ if (this._keys.length >= this._bucketCount * 0.75)
273
+ this.resize();
274
+ const h = getHashCode(key);
275
+ let idx = h & this._mask;
276
+ while (true) {
277
+ const entry = this._indices[idx];
278
+ // Found empty slot -> Insert new
279
+ if (entry === 0) {
280
+ this._hashes.push(h);
281
+ this._keys.push(key);
282
+ this._values.push(value);
283
+ this._indices[idx] = this._keys.length; // Store 1-based index
284
+ this._cachedHash = null;
285
+ return;
286
+ }
287
+ // Found existing key -> Update value
288
+ const ptr = entry - 1;
289
+ if (this._hashes[ptr] === h && areEqual(this._keys[ptr], key)) {
290
+ this._values[ptr] = value;
291
+ this._cachedHash = null;
292
+ return;
217
293
  }
294
+ // Collision -> Next slot
295
+ idx = (idx + 1) & this._mask;
218
296
  }
219
- return out;
220
297
  }
221
- #checkFrozen(op) {
222
- if (this.#isFrozen) {
223
- throw new Error(`InvalidOperation: Cannot ${op} a frozen RecursiveSet. Use mutableCopy().`);
298
+ get(key) {
299
+ const h = getHashCode(key);
300
+ let idx = h & this._mask;
301
+ while (true) {
302
+ const entry = this._indices[idx];
303
+ if (entry === 0)
304
+ return undefined;
305
+ const ptr = entry - 1;
306
+ if (this._hashes[ptr] === h && areEqual(this._keys[ptr], key))
307
+ return this._values[ptr];
308
+ idx = (idx + 1) & this._mask;
224
309
  }
225
310
  }
226
- /** * Returns the internal sorted array (readonly).
227
- * **Warning:** Readonly is only enforced by TypeScript.
228
- * Mutating the underlying array via `as any` breaks invariants (Binary Search/Sort rely on strict ordering).
229
- */
230
- get raw() { return this.#elements; }
231
- /** * Computes the hash code.
232
- * **Side Effect**: Freezes the set to prevent hash corruption.
233
- */
234
- get hashCode() {
235
- if (this.#hashCode !== null)
236
- return this.#hashCode;
237
- let h = 0;
238
- const arr = this.#elements;
239
- const len = arr.length;
240
- for (let i = 0; i < len; i++) {
241
- h = (Math.imul(31, h) + hashValue(arr[i])) | 0;
311
+ delete(key) {
312
+ if (this._isFrozen)
313
+ throw new Error("Frozen Map");
314
+ if (this.size === 0)
315
+ return false;
316
+ const h = getHashCode(key);
317
+ let idx = h & this._mask;
318
+ while (true) {
319
+ const entry = this._indices[idx];
320
+ if (entry === 0)
321
+ return false;
322
+ const ptr = entry - 1;
323
+ if (this._hashes[ptr] === h && areEqual(this._keys[ptr], key)) {
324
+ this._cachedHash = null;
325
+ // 1. Remove from index table (Backshift Deletion)
326
+ this.removeIndex(idx);
327
+ // 2. Remove from dense arrays (Swap with last element to keep arrays packed)
328
+ const lastKey = this._keys.pop();
329
+ const lastVal = this._values.pop();
330
+ const lastHash = this._hashes.pop();
331
+ if (ptr < this._keys.length) {
332
+ this._keys[ptr] = lastKey;
333
+ this._values[ptr] = lastVal;
334
+ this._hashes[ptr] = lastHash;
335
+ // Update index table to point to new location of the swapped element
336
+ this.updateIndexForKey(lastHash, this._keys.length + 1, ptr + 1);
337
+ }
338
+ return true;
339
+ }
340
+ idx = (idx + 1) & this._mask;
242
341
  }
243
- this.#hashCode = h;
244
- this.#isFrozen = true; // Freeze on hash access
245
- return h;
246
342
  }
247
- get isFrozen() { return this.#isFrozen; }
248
- get size() { return this.#elements.length; }
249
- isEmpty() { return this.#elements.length === 0; }
250
- /**
251
- * Compares this set with another set for ordering.
252
- * Uses hash comparison first, then deep structural comparison.
253
- */
254
- compare(other) {
255
- if (this === other)
256
- return 0;
257
- const h1 = this.hashCode;
258
- const h2 = other.hashCode;
259
- if (h1 !== h2)
260
- return h1 < h2 ? -1 : 1;
261
- const arrA = this.#elements;
262
- const arrB = other.#elements;
263
- const len = arrA.length;
264
- if (len !== arrB.length)
265
- return len - arrB.length;
266
- for (let i = 0; i < len; i++) {
267
- const diff = compare(arrA[i], arrB[i]);
268
- if (diff !== 0)
269
- return diff;
343
+ /** Updates the index table when an element is moved in the dense arrays. */
344
+ updateIndexForKey(hash, oldLoc, newLoc) {
345
+ let idx = hash & this._mask;
346
+ while (true) {
347
+ if (this._indices[idx] === oldLoc) {
348
+ this._indices[idx] = newLoc;
349
+ return;
350
+ }
351
+ idx = (idx + 1) & this._mask;
270
352
  }
271
- return 0;
272
353
  }
273
- equals(other) { return this.compare(other) === 0; }
274
354
  /**
275
- * Checks if element exists.
276
- * Uses Binary Search ($O(\log N)$) for larger sets, linear scan for small sets.
355
+ * Removes an entry from the hash table and repairs the probe chain.
356
+ * Uses "Backshift Deletion" (Robin Hood style) to fill the hole.
277
357
  */
278
- has(element) {
279
- const arr = this.#elements;
280
- const len = arr.length;
281
- if (len < 10) { // Linear scan optimization
282
- for (let i = 0; i < len; i++) {
283
- if (compare(arr[i], element) === 0)
284
- return true;
358
+ removeIndex(holeIdx) {
359
+ let i = (holeIdx + 1) & this._mask;
360
+ while (this._indices[i] !== 0) {
361
+ const entry = this._indices[i];
362
+ const ptr = entry - 1;
363
+ const h = this._hashes[ptr];
364
+ const ideal = h & this._mask;
365
+ const distHole = (holeIdx - ideal + this._bucketCount) & this._mask;
366
+ const distI = (i - ideal + this._bucketCount) & this._mask;
367
+ // If the element at 'i' belongs to a bucket logically before the hole,
368
+ // or is further away from its ideal slot than the hole is, move it back.
369
+ if (distHole < distI) {
370
+ this._indices[holeIdx] = entry;
371
+ holeIdx = i;
285
372
  }
373
+ i = (i + 1) & this._mask;
374
+ }
375
+ this._indices[holeIdx] = 0;
376
+ }
377
+ equals(other) {
378
+ if (this === other)
379
+ return true;
380
+ if (!(other instanceof RecursiveMap))
286
381
  return false;
382
+ if (this.size !== other.size)
383
+ return false;
384
+ if (this.hashCode !== other.hashCode)
385
+ return false;
386
+ for (let i = 0; i < this._keys.length; i++) {
387
+ const val = other.get(this._keys[i]);
388
+ if (val === undefined || !areEqual(this._values[i], val))
389
+ return false;
287
390
  }
288
- let low = 0, high = len - 1;
289
- while (low <= high) {
290
- const mid = (low + high) >>> 1;
291
- const cmp = compare(arr[mid], element);
292
- if (cmp === 0)
293
- return true;
294
- if (cmp < 0)
295
- low = mid + 1;
296
- else
297
- high = mid - 1;
391
+ return true;
392
+ }
393
+ *[Symbol.iterator]() {
394
+ for (let i = 0; i < this._keys.length; i++)
395
+ yield [this._keys[i], this._values[i]];
396
+ }
397
+ toString() {
398
+ if (this.isEmpty())
399
+ return `RecursiveMap(0) {}`;
400
+ // Sort only for visual output stability
401
+ const entries = this._keys.map((k, i) => ({ key: k, value: this._values[i] }));
402
+ entries.sort((a, b) => compareVisualLogic(a.key, b.key));
403
+ const body = entries
404
+ .map(e => ` ${String(e.key)} => ${String(e.value)}`)
405
+ .join(',\n');
406
+ return `RecursiveMap(${this.size}) {\n${body}\n}`;
407
+ }
408
+ [Symbol.for('nodejs.util.inspect.custom')]() { return this.toString(); }
409
+ }
410
+ exports.RecursiveMap = RecursiveMap;
411
+ // ============================================================================
412
+ // 6. RECURSIVE SET
413
+ // ============================================================================
414
+ /**
415
+ * A Hash Set implementation supporting Value Semantics.
416
+ * * Features:
417
+ * - O(1) amortized Add/Has/Remove.
418
+ * - Deep equality for recursive structures.
419
+ * - Frozen state protection after hashing.
420
+ */
421
+ class RecursiveSet {
422
+ _values = [];
423
+ _hashes = [];
424
+ _indices;
425
+ _bucketCount;
426
+ _mask;
427
+ _xorHash = 0;
428
+ _isFrozen = false;
429
+ LOAD_FACTOR = 0.75;
430
+ MIN_BUCKETS = 16;
431
+ constructor(...initialData) {
432
+ this._bucketCount = this.MIN_BUCKETS;
433
+ if (initialData.length > 0) {
434
+ const target = Math.ceil(initialData.length / this.LOAD_FACTOR);
435
+ while (this._bucketCount < target)
436
+ this._bucketCount <<= 1;
298
437
  }
299
- return false;
438
+ this._mask = this._bucketCount - 1;
439
+ this._indices = new Uint32Array(this._bucketCount);
440
+ for (const item of initialData)
441
+ this.add(item);
442
+ }
443
+ get size() { return this._values.length; }
444
+ get isFrozen() { return this._isFrozen; }
445
+ isEmpty() { return this._values.length === 0; }
446
+ get hashCode() {
447
+ if (this._isFrozen)
448
+ return this._xorHash;
449
+ // Deep-freeze trigger: touch child hashCodes once
450
+ for (let i = 0; i < this._values.length; i++) {
451
+ const v = this._values[i];
452
+ if (v && typeof v === 'object') {
453
+ v.hashCode; // triggers freeze of nested RecursiveSet/Map/Tuple/etc.
454
+ }
455
+ }
456
+ this._isFrozen = true;
457
+ return this._xorHash;
300
458
  }
301
459
  /**
302
- * Adds an element.
303
- * @throws if set is frozen.
304
- * Complexity: $O(N)$ (Array splice).
460
+ * Ensures the hash table has enough buckets to hold `capacity` elements
461
+ * without exceeding the load factor.
305
462
  */
306
- add(element) {
307
- this.#checkFrozen('add() to');
308
- const arr = this.#elements;
309
- // Optimization: Check last element first (append is common)
310
- if (arr.length > 0) {
311
- const lastCmp = compare(arr[arr.length - 1], element);
312
- if (lastCmp < 0) {
313
- arr.push(element);
314
- this.#hashCode = null;
315
- return this;
463
+ ensureCapacity(capacity) {
464
+ if (capacity * 1.33 > this._bucketCount) {
465
+ let target = this._bucketCount;
466
+ while (target * 0.75 < capacity)
467
+ target *= 2;
468
+ this._bucketCount = target;
469
+ this._mask = this._bucketCount - 1;
470
+ const oldValues = this._values;
471
+ const oldHashes = this._hashes;
472
+ this._indices = new Uint32Array(this._bucketCount);
473
+ for (let i = 0; i < oldValues.length; i++) {
474
+ const h = oldHashes[i];
475
+ let idx = h & this._mask;
476
+ while (this._indices[idx] !== 0)
477
+ idx = (idx + 1) & this._mask;
478
+ this._indices[idx] = i + 1;
316
479
  }
317
- if (lastCmp === 0)
318
- return this;
319
- }
320
- let low = 0, high = arr.length - 1, idx = arr.length;
321
- while (low <= high) {
322
- const mid = (low + high) >>> 1;
323
- const cmp = compare(arr[mid], element);
324
- if (cmp === 0)
325
- return this;
326
- if (cmp < 0)
327
- low = mid + 1;
328
- else {
329
- idx = mid;
330
- high = mid - 1;
480
+ }
481
+ }
482
+ resize() {
483
+ this.ensureCapacity(this._values.length + 1);
484
+ }
485
+ /** Updates the index table when a value moves in the dense array */
486
+ updateIndexForValue(hash, oldLoc, newLoc) {
487
+ let idx = hash & this._mask;
488
+ while (true) {
489
+ if (this._indices[idx] === oldLoc) {
490
+ this._indices[idx] = newLoc;
491
+ return;
492
+ }
493
+ idx = (idx + 1) & this._mask;
494
+ }
495
+ }
496
+ /** Repairs the probe chain after deletion (Backshifting) */
497
+ removeIndex(holeIdx) {
498
+ let i = (holeIdx + 1) & this._mask;
499
+ while (this._indices[i] !== 0) {
500
+ const entry = this._indices[i];
501
+ const valPtr = entry - 1;
502
+ const h = this._hashes[valPtr];
503
+ const idealIdx = h & this._mask;
504
+ const distToHole = (holeIdx - idealIdx + this._bucketCount) & this._mask;
505
+ const distToI = (i - idealIdx + this._bucketCount) & this._mask;
506
+ if (distToHole < distToI) {
507
+ this._indices[holeIdx] = entry;
508
+ holeIdx = i;
331
509
  }
510
+ i = (i + 1) & this._mask;
332
511
  }
333
- arr.splice(idx, 0, element);
334
- this.#hashCode = null;
335
- return this;
512
+ this._indices[holeIdx] = 0;
513
+ }
514
+ static compareVisual(a, b) {
515
+ return compareVisualLogic(a, b);
336
516
  }
337
517
  /**
338
- * Removes an element.
518
+ * Adds an element to the set.
519
+ * @returns void to signal no chaining (performance).
339
520
  * @throws if set is frozen.
340
- * Complexity: $O(N)$.
341
521
  */
342
- remove(element) {
343
- this.#checkFrozen('remove() from');
344
- const arr = this.#elements;
345
- let low = 0, high = arr.length - 1;
346
- while (low <= high) {
347
- const mid = (low + high) >>> 1;
348
- const cmp = compare(arr[mid], element);
349
- if (cmp === 0) {
350
- arr.splice(mid, 1);
351
- this.#hashCode = null;
352
- return this;
522
+ add(e) {
523
+ if (this._isFrozen)
524
+ throw new Error("Frozen Set modified.");
525
+ if (this._values.length >= this._bucketCount * this.LOAD_FACTOR)
526
+ this.resize();
527
+ const h = getHashCode(e);
528
+ let idx = h & this._mask;
529
+ while (true) {
530
+ const entry = this._indices[idx];
531
+ if (entry === 0) {
532
+ this._hashes.push(h);
533
+ this._values.push(e);
534
+ this._indices[idx] = this._values.length;
535
+ this._xorHash ^= h;
536
+ return;
353
537
  }
354
- if (cmp < 0)
355
- low = mid + 1;
356
- else
357
- high = mid - 1;
538
+ const valIndex = entry - 1;
539
+ if (this._hashes[valIndex] === h && areEqual(this._values[valIndex], e))
540
+ return;
541
+ idx = (idx + 1) & this._mask;
542
+ }
543
+ }
544
+ has(element) {
545
+ const h = getHashCode(element);
546
+ let idx = h & this._mask;
547
+ while (true) {
548
+ const entry = this._indices[idx];
549
+ if (entry === 0)
550
+ return false;
551
+ const valIndex = entry - 1;
552
+ if (this._hashes[valIndex] === h && areEqual(this._values[valIndex], element))
553
+ return true;
554
+ idx = (idx + 1) & this._mask;
358
555
  }
359
- return this;
360
556
  }
361
- clear() {
362
- this.#checkFrozen('clear()');
363
- this.#elements = [];
364
- this.#hashCode = 0;
365
- return this;
557
+ remove(e) {
558
+ if (this._isFrozen)
559
+ throw new Error("Frozen Set modified.");
560
+ if (this._values.length === 0)
561
+ return;
562
+ const h = getHashCode(e);
563
+ let idx = h & this._mask;
564
+ while (true) {
565
+ const entry = this._indices[idx];
566
+ if (entry === 0)
567
+ return;
568
+ const valIndex = entry - 1;
569
+ if (this._hashes[valIndex] === h && areEqual(this._values[valIndex], e)) {
570
+ this._xorHash ^= h;
571
+ this.removeIndex(idx);
572
+ const lastVal = this._values.pop();
573
+ const lastHash = this._hashes.pop();
574
+ if (valIndex < this._values.length) {
575
+ this._values[valIndex] = lastVal;
576
+ this._hashes[valIndex] = lastHash;
577
+ this.updateIndexForValue(lastHash, this._values.length + 1, valIndex + 1);
578
+ }
579
+ return;
580
+ }
581
+ idx = (idx + 1) & this._mask;
582
+ }
366
583
  }
367
- mutableCopy() {
584
+ clone() {
368
585
  const s = new RecursiveSet();
369
- s.#elements = this.#elements.slice();
586
+ if (s._bucketCount !== this._bucketCount)
587
+ s.ensureCapacity(this.size);
588
+ if (s._bucketCount === this._bucketCount) {
589
+ s._indices.set(this._indices);
590
+ s._values = this._values.slice();
591
+ s._hashes = this._hashes.slice();
592
+ s._xorHash = this._xorHash;
593
+ }
594
+ else {
595
+ for (const v of this._values)
596
+ s.add(v);
597
+ }
370
598
  return s;
371
599
  }
372
- clone() { return this.mutableCopy(); }
373
- // === OPTIMIZED SET OPERATIONS (Merge Scan) ===
600
+ // ========================================================================
601
+ // === FUNCTIONAL METHODS (Optimized) ===
602
+ // ========================================================================
374
603
  /**
375
- * Computes Union $A \cup B$.
376
- * Implementation: Merge Scan.
377
- * Complexity: $O(|A| + |B|)$.
604
+ * Tests whether all elements in the set pass the test implemented by the provided function.
605
+ * Uses internal iteration for max speed.
378
606
  */
379
- union(other) {
380
- const A = this.#elements;
381
- const B = other.raw; // Efficient access via getter
382
- const res = [];
383
- let i = 0, j = 0;
384
- const lenA = A.length, lenB = B.length;
385
- while (i < lenA && j < lenB) {
386
- const cmp = compare(A[i], B[j]);
387
- if (cmp < 0)
388
- res.push(A[i++]);
389
- else if (cmp > 0)
390
- res.push(B[j++]);
391
- else {
392
- res.push(A[i++]);
393
- j++;
394
- }
607
+ every(predicate) {
608
+ // Direct array access avoids iterator generator overhead
609
+ for (let i = 0; i < this._values.length; i++) {
610
+ if (!predicate(this._values[i]))
611
+ return false;
395
612
  }
396
- while (i < lenA)
397
- res.push(A[i++]);
398
- while (j < lenB)
399
- res.push(B[j++]);
400
- return RecursiveSet.fromSortedUnsafe(res);
613
+ return true;
401
614
  }
402
615
  /**
403
- * Computes Intersection $A \cap B$.
404
- * Implementation: Synchronous Scan.
405
- * Complexity: $O(|A| + |B|)$.
616
+ * Tests whether at least one element in the set passes the test.
406
617
  */
407
- intersection(other) {
408
- const A = this.#elements;
409
- const B = other.raw;
410
- const res = [];
411
- let i = 0, j = 0;
412
- const lenA = A.length, lenB = B.length;
413
- while (i < lenA && j < lenB) {
414
- const cmp = compare(A[i], B[j]);
415
- if (cmp < 0)
416
- i++;
417
- else if (cmp > 0)
418
- j++;
419
- else {
420
- res.push(A[i++]);
421
- j++;
422
- }
618
+ some(predicate) {
619
+ for (let i = 0; i < this._values.length; i++) {
620
+ if (predicate(this._values[i]))
621
+ return true;
423
622
  }
424
- return RecursiveSet.fromSortedUnsafe(res);
623
+ return false;
425
624
  }
426
625
  /**
427
- * Computes Difference $A \setminus B$.
428
- * Complexity: $O(|A| + |B|)$.
626
+ * Creates a new RecursiveSet populated with the results of calling a provided function.
627
+ * Pre-allocates storage to prevent resizing.
429
628
  */
430
- difference(other) {
431
- const A = this.#elements;
432
- const B = other.raw;
433
- const res = [];
434
- let i = 0, j = 0;
435
- const lenA = A.length, lenB = B.length;
436
- while (i < lenA && j < lenB) {
437
- const cmp = compare(A[i], B[j]);
438
- if (cmp < 0)
439
- res.push(A[i++]);
440
- else if (cmp > 0)
441
- j++;
442
- else {
443
- i++;
444
- j++;
445
- }
629
+ map(callback) {
630
+ const result = new RecursiveSet();
631
+ result.ensureCapacity(this.size);
632
+ for (let i = 0; i < this._values.length; i++) {
633
+ result.add(callback(this._values[i]));
446
634
  }
447
- while (i < lenA)
448
- res.push(A[i++]);
449
- return RecursiveSet.fromSortedUnsafe(res);
635
+ return result;
450
636
  }
451
637
  /**
452
- * Computes Symmetric Difference $A \triangle B$.
453
- * Complexity: $O(|A| + |B|)$.
638
+ * Creates a new RecursiveSet with all elements that pass the test.
454
639
  */
455
- symmetricDifference(other) {
456
- const A = this.#elements;
457
- const B = other.raw;
458
- const res = [];
459
- let i = 0, j = 0;
460
- const lenA = A.length, lenB = B.length;
461
- while (i < lenA && j < lenB) {
462
- const cmp = compare(A[i], B[j]);
463
- if (cmp < 0)
464
- res.push(A[i++]);
465
- else if (cmp > 0)
466
- res.push(B[j++]);
467
- else {
468
- i++;
469
- j++;
470
- }
640
+ filter(predicate) {
641
+ const result = new RecursiveSet();
642
+ for (let i = 0; i < this._values.length; i++) {
643
+ const v = this._values[i];
644
+ if (predicate(v))
645
+ result.add(v);
471
646
  }
472
- while (i < lenA)
473
- res.push(A[i++]);
474
- while (j < lenB)
475
- res.push(B[j++]);
476
- return RecursiveSet.fromSortedUnsafe(res);
647
+ return result;
477
648
  }
478
649
  /**
479
- * Computes Cartesian Product $A \times B$.
480
- * Complexity: $O(|A| \cdot |B| \cdot \log(|A| \cdot |B|))$ (due to sorting).
650
+ * Executes a reducer function on each element.
481
651
  */
652
+ reduce(callback, initialValue) {
653
+ let accumulator = initialValue;
654
+ for (let i = 0; i < this._values.length; i++) {
655
+ accumulator = callback(accumulator, this._values[i]);
656
+ }
657
+ return accumulator;
658
+ }
659
+ // === SET OPERATIONS ===
660
+ union(other) {
661
+ const res = this.clone();
662
+ for (const v of other._values)
663
+ res.add(v);
664
+ return res;
665
+ }
666
+ intersection(other) {
667
+ const res = new RecursiveSet();
668
+ // Iterate over smaller set for O(min(N, M))
669
+ const [small, large] = this.size < other.size ? [this, other] : [other, this];
670
+ for (const v of small._values) {
671
+ if (large.has(v))
672
+ res.add(v);
673
+ }
674
+ return res;
675
+ }
676
+ difference(other) {
677
+ if (other.isEmpty())
678
+ return this.clone();
679
+ const res = new RecursiveSet();
680
+ for (const v of this._values) {
681
+ if (!other.has(v))
682
+ res.add(v);
683
+ }
684
+ return res;
685
+ }
686
+ symmetricDifference(other) {
687
+ const res = new RecursiveSet();
688
+ for (const v of this._values) {
689
+ if (!other.has(v))
690
+ res.add(v);
691
+ }
692
+ for (const v of other._values) {
693
+ if (!this.has(v))
694
+ res.add(v);
695
+ }
696
+ return res;
697
+ }
482
698
  cartesianProduct(other) {
483
- const result = [];
484
- const arrA = this.#elements;
485
- const arrB = other.raw;
486
- const lenA = arrA.length;
487
- const lenB = arrB.length;
488
- for (let i = 0; i < lenA; i++) {
489
- const a = arrA[i];
490
- for (let j = 0; j < lenB; j++) {
491
- result.push(new Tuple(a, arrB[j]));
699
+ const result = new RecursiveSet();
700
+ result.ensureCapacity(this.size * other.size);
701
+ for (const a of this) {
702
+ for (const b of other) {
703
+ result.add(new Tuple(a, b));
492
704
  }
493
705
  }
494
- // Hashes are not monotonic, so we MUST sort.
495
- result.sort(compare);
496
- // But uniqueness is guaranteed mathematically, so use unsafe create
497
- return RecursiveSet.fromSortedUnsafe(result);
706
+ return result;
498
707
  }
499
708
  /**
500
- * Computes the Power Set $\mathcal{P}(A)$.
501
- * Complexity: $O(2^N)$.
502
- * @throws if size > 20.
709
+ * Computes the Power Set.
710
+ * Warning: Complexity is O(2^N). Use only for small N.
711
+ * @throws Error if size > 20 to prevent memory exhaustion and 32-bit shift overflow.
503
712
  */
504
713
  powerset() {
505
- const n = this.size;
506
- if (n > 20)
507
- throw new Error("Powerset too large");
508
- const subsets = [];
714
+ const n = this._values.length;
715
+ if (n > 20) {
716
+ throw new Error(`Powerset size too large (N=${n}). Max supported is 20.`);
717
+ }
509
718
  const max = 1 << n;
719
+ const result = new RecursiveSet();
720
+ result.ensureCapacity(max);
510
721
  for (let i = 0; i < max; i++) {
511
- const subsetElements = [];
722
+ const subset = new RecursiveSet();
512
723
  for (let j = 0; j < n; j++) {
513
- if (i & (1 << j))
514
- subsetElements.push(this.#elements[j]);
724
+ if ((i & (1 << j)) !== 0) {
725
+ subset.add(this._values[j]);
726
+ }
515
727
  }
516
- // Elements inside subset are already sorted -> Unsafe
517
- subsets.push(RecursiveSet.fromSortedUnsafe(subsetElements));
728
+ result.add(subset);
518
729
  }
519
- // Subsets themselves need sorting
520
- return RecursiveSet.fromArray(subsets);
730
+ return result;
731
+ }
732
+ pickRandom() {
733
+ if (this._values.length === 0)
734
+ return undefined;
735
+ return this._values[Math.floor(Math.random() * this._values.length)];
521
736
  }
522
737
  isSubset(other) {
523
738
  if (this.size > other.size)
524
739
  return false;
525
- let i = 0, j = 0;
526
- const A = this.#elements, B = other.raw;
527
- while (i < A.length && j < B.length) {
528
- const cmp = compare(A[i], B[j]);
529
- if (cmp < 0)
740
+ if (this === other)
741
+ return true;
742
+ for (const item of this._values) {
743
+ if (!other.has(item))
530
744
  return false;
531
- if (cmp > 0)
532
- j++;
533
- else {
534
- i++;
535
- j++;
536
- }
537
745
  }
538
- return i === A.length;
746
+ return true;
747
+ }
748
+ isSuperset(other) {
749
+ return other.isSubset(this);
750
+ }
751
+ [Symbol.iterator]() { return this._values[Symbol.iterator](); }
752
+ equals(other) {
753
+ if (this === other)
754
+ return true;
755
+ if (!(other instanceof RecursiveSet))
756
+ return false;
757
+ if (this.size !== other.size)
758
+ return false;
759
+ if (this.hashCode !== other.hashCode)
760
+ return false;
761
+ for (const v of this._values) {
762
+ if (!other.has(v))
763
+ return false;
764
+ }
765
+ return true;
766
+ }
767
+ toString() {
768
+ // Use compareVisual exclusively for user-facing output
769
+ const sorted = [...this._values].sort(compareVisualLogic);
770
+ return `{${sorted.map(String).join(', ')}}`;
539
771
  }
540
- isSuperset(other) { return other.isSubset(this); }
541
- /** Iterates over set elements in sorted order. */
542
- *[Symbol.iterator]() { yield* this.#elements; }
543
- /** Returns string representation e.g. "{1, 2, 3}". */
544
- toString() { return `{${this.#elements.join(', ')}}`; }
545
- /** Custom inspection for Node.js console to avoid printing internal state. */
546
772
  [Symbol.for('nodejs.util.inspect.custom')]() { return this.toString(); }
547
773
  }
548
774
  exports.RecursiveSet = RecursiveSet;
549
- // Exports
550
- /** Factory: Creates an empty RecursiveSet */
775
+ // ============================================================================
776
+ // 7. PUBLIC EXPORTS
777
+ // ============================================================================
551
778
  function emptySet() { return new RecursiveSet(); }
552
- /** Factory: Creates a singleton RecursiveSet containing {element} */
553
- function singleton(element) { return new RecursiveSet(element); }
779
+ function singleton(el) { return new RecursiveSet(el); }
780
+ const hashValue = getHashCode;
781
+ exports.hashValue = hashValue;
554
782
  //# sourceMappingURL=index.js.map